Search code examples
kotlinkotlin-coroutinescoroutine

Doc: Is it correct that the thread running runBlocking blocks?


The docs on Your first coroutine say -

The name of runBlocking means that the thread that runs it (in this case — the main thread) gets blocked for the duration of the call, until all the coroutines inside runBlocking { ... } complete their execution

Is it correct that the thread running runBlocking blocks?

If the main thread is blocked, then how the coroutine launched by launch { } in the below code (from the same doc) runs on the main thread??

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println(Thread.currentThread().name + " World!") // print after delay
    }
    println(Thread.currentThread().name + " Hello") // main coroutine continues while a previous one is delayed
}

My understanding is that

  • launch{ } created a child coroutine in runBlocking scope
  • and runBlocking waits for the completion of this child coroutine, unlike builders like launch{ } or async { } that immediately return after creating the new coroutine
  • but runBlocking frees the main thread
  • since main thread is free, the new coroutine created by runBlocking also runs on main thread

Here is an output from the above code -

main @coroutine#1 Hello
main @coroutine#2 World!

Same issue in the documentation of runBlocking

Runs a new coroutine and blocks the current thread until its completion. This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.


Solution

  • Compare the documentation you posted for runBlocking

    Runs a new coroutine and blocks the current thread until its completion.

    and launch

    Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job.

    They both create a new coroutine, it's just that launch runs its coroutine asynchronously, and allows the caller to continue running code. runBlocking doesn't allow the caller to continue until everything running in that coroutine has finished. So runBlocking doesn't control the behaviour of the stuff inside its coroutine, just the execution of the code that's calling runBlocking.

    This tweak to your code might make things clearer:

    fun main() {
        runBlocking {
            launch {
                delay(1000L)
                println(Thread.currentThread().name + " World!")
            }
            println(Thread.currentThread().name + " Hello")
        }
        // this -follows- the runBlocking statement
        println("Hey I'm runnin' here!")
    }
    
    main @coroutine#1 Hello
    main @coroutine#2 World!
    Hey I'm runnin' here!
    

    So what's happening is:

    • runBlocking runs, blocking the main thread. The next line won't run until everything in this coroutine has finished!
    • launch runs, creating a new coroutine on the same thread, but without blocking it. The code on the next line will be able to run while this is running, because they're in separate coroutines, but taking turns in little time slices (since they're on the same thread). This coroutine calls delay immediately, so even if it runs before the line in the original coroutine (the println after launch) it'll definitely hit its own println much later
    • the println following launch runs, since this coroutine is independent of the other one. This coroutine is finished (but the other one is still running)
    • the println inside launch runs after its delay. This coroutine has nothing else to do, so it's also finished
    • since all the Jobs in the coroutine launched by runBlocking have finished, that blocking coroutine ends, and execution can continue in the scope where it was launched
    • that last println gets to run, finally!

    Also just to clear up a possible misunderstanding:

    Is it correct that the thread running runBlocking blocks?

    If the main thread is blocked, then how the coroutine launched by launch { } in the >below code (from the same doc) runs on the main thread??

    All coroutines run on the same thread as the caller, unless you explicitly tell them not to, or use a dispatcher that can move them to a different thread. It's just that the coroutines "take turns" and get a bit of time to execute some code on a thread. So it looks like they're running "at the same time" even when they're not.

    So because launch doesn't "block", what that means is the coroutine that calls it is allowed to continue, getting a bit of time to execute the rest of the code, while the launched coroutine also gets some time slots. They can run independently.

    But runBlocking does block, which means it doesn't get any execution time until the coroutine it started finishes. It takes a break until the coroutine is done. So it's not actually blocking the main thread in the normal sense, otherwise none of the coroutines on that thread could run! It's just blocking execution in its own scope - it's a way to say "nothing else happens here until this job has finished". That's why it's a common main function wrapper, to kind of create a world of coroutines that everything runs in, and preventing the main function from completing and ending the program. But it does block anything outside that from running, which is why it's a bad idea in something like Android, where there's other stuff going on with the main thread besides any coroutines you might want to run.

    Also, launch actually runs on a CoroutineScope (i.e. CoroutineScope.launch) , which it has access to inside a coroutine - that's why you have to use it inside one! runBlocking is a standalone builder function, but the function block you pass in has a type of CoroutineScope.() -> T - i.e. it's going to be passed to a CoroutineScope, like a default one provided by the framework. That CoroutineScope is what works out when all its child coroutines have finished, and informs the caller that it's done. runBlocking just waits around for that to happen, instead of allowing execution to continue like the coroutineScope builder.