Issue
It's quite clearly stated in official documentation that runBlocking
"should not be used from a coroutine". I roughly get the idea, but I'm trying to find an example where using runBlocking
instead of suspend functions negatively impacts performance.
So I created an example like this:
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.time.Duration.Companion.seconds
private val time = 1.seconds
private suspend fun getResource(name: String): String {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
return "Resource ${name}"
}
fun main(args: Array<String>) = runBlocking {
val resources = listOf("A", "B")
.map { async { getResource(it) } }
.awaitAll()
log(resources)
}
fun log(msg: Any) {
val now = DateTimeFormatter.ISO_INSTANT.format(Instant.now())
println("$now ${Thread.currentThread()}: $msg")
}
This gives the expected output of:
2022-04-29T15:52:35.943156Z Thread[main,5,main]: Starting getting A for 1s...
2022-04-29T15:52:35.945570Z Thread[main,5,main]: Starting getting B for 1s...
2022-04-29T15:52:36.947539Z Thread[main,5,main]: Finished getting A!
2022-04-29T15:52:36.948334Z Thread[main,5,main]: Finished getting B!
2022-04-29T15:52:36.949233Z Thread[main,5,main]: [Resource A, Resource B]
From my understanding getResource(A)
was started and the moment it arrived at delay
it gave the control back and then getResource(B)
was started. Then they both waited in a single thread and when the time passed, they both were again executed - everything in one second as expected.
So now I wanted to "break" it a little and replaced getResource
with:
private fun getResourceBlocking(name: String): String = runBlocking {
log("Starting getting ${name} for ${time}...")
delay(time)
log("Finished getting ${name}!")
"Resource ${name}"
}
and called it from the main
method in place of getResource
.
and then again I got:
2022-04-29T15:58:41.908015Z Thread[main,5,main]: Starting getting A for 1s...
2022-04-29T15:58:41.910532Z Thread[main,5,main]: Starting getting B for 1s...
2022-04-29T15:58:42.911661Z Thread[main,5,main]: Finished getting A!
2022-04-29T15:58:42.912126Z Thread[main,5,main]: Finished getting B!
2022-04-29T15:58:42.912876Z Thread[main,5,main]: [Resource A, Resource B]
So it still took only 1 second to run and B
was started before A
finished. At the same time there doesn't seem to be any additional threads spawned (everything is in Thread[main,5,main]
).
So how does this work? How calling blocking functions in async
makes it execute "concurrently" in a single thread anyway?
Solution
Your reasoning is correct, but you accidentally hit a very special case of using runBlocking()
, which was intentionally optimized to not degrade the performance. If you use dispatcher-less runBlocking()
inside another dispatcher-less runBlocking()
, then the inner runBlocking()
tries to re-use the event loop created by the outer one. So inner runBlocking()
actually works similarly as it is suspending and not blocking (but this is not 100% accurate).
In a real case where the outer coroutine would not be itself created with runBlocking()
or if you use some real dispatchers, you would see the degraded performance. You can replace the outer code with this:
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun main(args: Array<String>) = runBlocking(dispatcher) { ... }
Then resources are loaded sequentially, as you probably expected. But even with this change, getResource()
still loads resources concurrently.
Answered By - broot
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.