Home > Mobile >  In which aspects runBlocking is worse than suspend?
In which aspects runBlocking is worse than suspend?

Time:04-30

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?

CodePudding user response:

The goal with suspending is not to finish the work faster. The goal with suspending is to not block the current thread while waiting for the suspend function to return.

In your example, it doesn't matter if the current thread is blocked or not because it's not doing anything else while waiting.

In an app with a UI, you are usually concerned about blocking on the UI thread (AKA main thread), because that will freeze the app. Nothing can be animated, scrolled, or clicked while the UI thread is blocked.

If you call a suspend function from the main thread, the main thread will not be blocked while it waits for the suspend function to return.


The reason no other threads are spawned in your example is that you are using runBlocking which runs on the current thread by default without doing its work on a background thread. In an actual application, you would be launching coroutines from a CoroutineScope rather than from a runBlocking.

CodePudding user response:

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.

  • Related