Archie G. Quiñones
Archie G. Quiñones

Reputation: 13668

Coroutines: runBlocking vs coroutineScope

I was reading Coroutine Basics trying to understand and learn it.

There is a part there with this code:

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(900L) 
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }

    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

The output goes like so:

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

My question is why this line:

 println("Coroutine scope is over") // This line is not printed until nested launch completes

is called always last?

Shouldn't it be called since the:

coroutineScope { // Creates a new coroutine scope
    ....
}

is suspended?

There is also a note there:

The main difference between runBlocking and coroutineScope is that the latter does not block the current thread while waiting for all children to complete.

I dont understand how coroutineScope and runBlocking are different here? coroutineScope looks like its blocking since it only gets to the last line when it is done.

Can anyone enlighten me here?

Upvotes: 121

Views: 48713

Answers (7)

Shubham Kumar Gupta
Shubham Kumar Gupta

Reputation: 1147

suspend fun A() {
        Log.d("Test", "suspend A started")
        delay(5000)
        Log.d("Test", "suspend A over")
    }

    suspend fun B() {
        Log.d("Test", "suspend B")
        delay(1000)
        Log.d("Test", "suspend B over")
    }

    suspend fun D() {
        Log.d("Test", "suspend D")
        delay(1000)
        Log.d("Test", "suspend D over")
    }

    suspend fun E() {
        Log.d("Test", "suspend E")
        delay(1000)
        Log.d("Test", "suspend E over")
    }
fun main3() {
        runBlocking {
            // suspend = async + await = launch + join
            // suspend != async (async runs and returns when it has)
            // all launch will run parallel
    
            launch{
                A()
            }.join()
            
            launch{
                D()
            }

            launch {
                E()
            }

            async { // Extension of a coroutine Scope not its own scope
                Log.d("Test", "async C started")
                delay(1000)
                Log.d("Test", "async C over")
            }.await()

            launch{
                B()
            }
        }
        Log.d("Test", "main3 over")
    }

Result:

2024-02-16 17:39:16.759  suspend A started
2024-02-16 17:39:21.760  suspend A over
2024-02-16 17:39:21.762  suspend D
2024-02-16 17:39:21.762  suspend E
2024-02-16 17:39:21.762  async C started
2024-02-16 17:39:22.762  suspend D over
2024-02-16 17:39:22.762  suspend E over
2024-02-16 17:39:22.762  async C over
2024-02-16 17:39:22.763  suspend B
2024-02-16 17:39:23.763  suspend B over
2024-02-16 17:39:23.764  main3 over
    fun main4() {
        CoroutineScope(Dispatchers.IO).launch {
            // suspend = async + await = launch + join
            // suspend != async (async runs and returns when it has)
            launch{
                A()
            }.join()

            launch{
                D()
            }

            launch {
                E()
            }

            async {  // Extension of a coroutine Scope not its own scope
                Log.d("Test", "async C started")
                delay(1000)
                Log.d("Test", "async C over")
            }.await()

            launch{
                B()
            }
        }
        Log.d("Test", "main4 over")
    }

Result:

2024-02-16 17:34:20.141  main4 over
2024-02-16 17:34:20.142  suspend A started
2024-02-16 17:34:25.144  suspend A over
2024-02-16 17:34:25.145  suspend D
2024-02-16 17:34:25.145  async C started
2024-02-16 17:34:25.145  suspend E
2024-02-16 17:34:26.146  suspend D over
2024-02-16 17:34:26.146  async C over
2024-02-16 17:34:26.146  suspend E over
2024-02-16 17:34:26.146  suspend B
2024-02-16 17:34:27.147  suspend B over

Upvotes: 0

zwh
zwh

Reputation: 182

Your question about why println("Coroutine scope is over") called always last is not relevant about the difference between runBlocking vs coroutineScope. The accepted answer only explains the the difference between runBlocking vs coroutineScope. I will try to explain the other one.

First, I guess you misrepresent coroutineScope and launch.

  1. coroutineScope is a suspend function, it will suspend and not resume until all codes inside it are finished.
  2. launch is a normal function, it run suspend actions concurrently.

So the codes inside coroutineScope, two println will run in parallel.

coroutineScope { // Creates a new coroutine scope
    launch {
        delay(900L) 
        println("Task from nested launch")
    }

    delay(100L)
    println("Task from coroutine scope") // This line will be printed before nested launch
}

And with delay actions, the second one will be run first.

What bothering you is here. Why the first println run, but the last one don't?

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(900L) 
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }

    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

Because launch is a normal function, and coroutineScope is a suspend function. So the first println will not be blocked. If you change the launch to coroutineScope, the second coroutineScope will be blocked until the first one is resumed and return.


I think the difficultity of kotlin coroutine is that:

  1. there are several terms are hard to understand.
  2. apis in lambda style hide many details, it is easy to use but lose calling context about where you are.

Upvotes: 1

Marko Topolnik
Marko Topolnik

Reputation: 200158

I don't understand how coroutineScope and runBlocking are different here? coroutineScope looks like its blocking since it only gets to the last line when it is done.

There are two separate worlds: the suspendable world (within a coroutine) and the non-suspendable one. As soon as you enter the body of runBlocking, you are in the suspendable world, where suspend funs behave like blocking code and you can't get to the next line until the suspend fun returns. coroutineScope is a suspend fun that returns only when all the coroutines inside it are done. Therefore the last line must print at the end.


I copied the above explanation from a comment which seems to have clicked with readers. Here is the original answer:

From the perspective of the code in the block, your understanding is correct. The difference between runBlocking and coroutineScope happens at a lower level: what's happening to the thread while the coroutine is blocked?

  • runBlocking is not a suspend fun. The thread that called it remains inside it until the coroutine is complete.

  • coroutineScope is a suspend fun. If your coroutine suspends, the coroutineScope function gets suspended as well. This allows the top-level function, a non-suspending function that created the coroutine, to continue executing on the same thread. The thread has "escaped" the coroutineScope block and is ready to do some other work.

In your specific example: when your coroutineScope suspends, control returns to the implementation code inside runBlocking. This code is an event loop that drives all the coroutines you started within it. In your case, there will be some coroutines scheduled to run after a delay. When the time arrives, it will resume the appropriate coroutine, which will run for a short while, suspend, and then control will be again inside runBlocking.


While the above describes the conceptual similarities, it should also show you that runBlocking is a completely different tool from coroutineScope.

  • runBlocking is a low-level construct, to be used only in framework code or self-contained examples like yours. It turns an existing thread into an event loop and creates its coroutine with a Dispatcher that posts resuming coroutines to the event loop's queue.

  • coroutineScope is a user-facing construct, used to delineate the boundaries of a task that is being parallel-decomposed inside it. You use it to conveniently await on all the async work happening inside it, get the final result, and handle all failures at one central place.

Upvotes: 111

Bahaa
Bahaa

Reputation: 1757

Well, after having read all the answers here, I found none of them answered the question beyond repeating the wording of the fragments of the documentation.

So, I went on to search for an answer elsewhere and found it here. It practically shows the difference in behavior of coroutineScope and runBlocking (i.e. the difference between suspending and blocking)

Upvotes: 15

Johann
Johann

Reputation: 29867

The chosen answer is good but fails to address some other important aspects of the sample code that was provided. For instance, launch is non-blocking and is suppose to execute immediately. That is simply not true. The launch itself returns immediately BUT the code inside the launch does appear to be put into a queue and is only executed when any other launches that were previously put into the queue have completed.

Here's a similar piece of sample code with all the delays removed and an additional launch included. Without looking at the result below, see if you can predict the order in which the numbers are printed. Chances are that you will fail:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { 
        println("1")
    }

    coroutineScope {
        launch {
            println("2")
        }

        println("3") 
    }

    coroutineScope {
        launch {
            println("4")
        }

        println("5")
    }

    launch { 
        println("6")
    }

    for (i in 7..100) {
        println(i.toString())
    }

    println("101")
}

The result is:

3
1
2
5
4
7
8
9
10
...
99
100
101
6

The fact that number 6 is printed last, even after going through nearly 100 println have been executed, indicates that the code inside the last launch never gets executed until all non-blocking code after the launch has completed. But that is not really true either, because if that were the case, the first launch should not have executed until numbers 7 to 101 have completed. Bottom line? Mixing launch and coroutineScope is highly unpredictable and should be avoided if you expect a certain order in the way things should be executed.

To prove that code inside launches is placed into a queue and ONLY executed after ALL the non-blocking code has completed, run this (no coroutineScopes are used):

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { 
        println("1")
    }

    launch { 
        println("2")
    }

    launch { 
        println("3")
    }

    for (i in 4..100) {
        println(i.toString())
    }

    println("101")
}

This is the result you get:

4
5
6
...
101
1
2
3

Adding a CoroutineScope will break this behavior. It will cause all non-blocking code that follows the CoroutineScope to not be executed until ALL code prior to the CoroutineScope has completed.

It should also be noted that in this code sample, each of the launches in the queue are executed sequentially in the order that they are added to the queue and each launch will only execute AFTER the previous launch executes. This may make it appear that all launches share a common thread. This is not true. Each of them is given their own thread. However, if any code inside a launch calls a suspend function, the next launch in the queue is started immediately while the suspend function is being carried out. To be honest, this is very strange behavior. Why not just run all the launches in the queue asynchronously? While I don't know the internals of what goes on in this queue, my guess is that each launch in the queue does not get its own thread but all share a common thread. It is only when a suspend function is encountered does it appear that a new thread is created for the next launch in the queue. It may be done this way to save on resources.

To summarize, execution is done in this order:

  1. Code inside a launch is placed inside a queue and are executed in the order that they are added.
  2. Non-blocking code following a launch executes immediately before anything in the queue is executed.
  3. A CoroutineScope blocks ALL code following it BUT will execute all the launch coroutines in the queue before resuming to the code following the CoroutineScope.

Upvotes: 53

onmyway133
onmyway133

Reputation: 48085

From this wonderful article https://jivimberg.io/blog/2018/05/04/parallel-map-in-kotlin/

suspend fun <A, B> Iterable<A>.pmap(f: suspend (A) -> B): List<B> = coroutineScope {
    map { async { f(it) } }.awaitAll()
}

With runBlocking, we were not using Structured Concurrency, so an invocation of f could fail and all other executions would continue unfazed. And also we were not playing nice with the rest of the code. By using runBlocking we were forcefully blocking the thread until the whole execution of pmap finishes, instead of letting the caller decide how the execution should go.

Upvotes: 3

nyarian
nyarian

Reputation: 4365

runBlocking just blocks the current thread until inner coroutines will be completed. Here, thread that executes runBlocking will be blocked until the coroutine from coroutineScope will be finished.

First launch just won't allow the thread execute instructions that come after runBlocking, but will allow proceed to the instructions that come immediately after this launch block - that's why Task from coroutine scope is printed before than Task from runBlocking.

But nested coroutineScope in the context of runBlocking won't allow the thread to execute instructions that come after this coroutineScope block, because runBlocking will block the thread until the coroutine from coroutineScope will be finished completely. And that's why Coroutine scope is over will always come after Task from nested launch.

Upvotes: 8

Related Questions