hc0re
hc0re

Reputation: 1986

Kotlin Coroutines: runBlocking vs. coroutineScope

I am playing around with Kotlin Coroutines, and I ended up in a situation I do not understand. Let's say that I have two suspend functions:

suspend fun coroutine() {
    var num = 0
    coroutineScope {
        for (i in 1..1000) {
            launch {
                delay(10)
                num += 1
            }
        }
    }
    println("coroutine: $num")
}

and:

suspend fun runBlocked() = runBlocking {
    var num = 0
    for (i in 1..1000) {
        launch {
            delay(10)
            num += 1
        }
    }
    println("Run blocking: $num")
}

Then I call them from main() method:

suspend fun main() {
    coroutine()
    runBlocked()
}

The coroutine() method prints (as expected) a number that is almost never 1000 (usually between 970 and 999). And I understand why.

What I do not understand is why the runBlocked() function allways prints 0.

coroutine: 998
runBlocked: 0

I tried one more time, this time making a similar function to runBlocked(), with the difference that this time the method is returning a value instead of printing:

suspend fun runBlockedWithReturn(): Int = runBlocking {
    var num = 0
    for (i in 1..1000) {
        launch {
            delay(10)
            num += 1
        }
    }
    return@runBlocking num
}

And then I called it from the main() method:

suspend fun main() {
    val result = runBlockedWithReturn()
    println("Run blocking with return: $result")
}

...but the method returned 0.

Why is that? And how do I fix the runBlocked() method to print a number that is close to 1000 instead of 0? What am I missing?

Upvotes: 1

Views: 1662

Answers (3)

Tenfour04
Tenfour04

Reputation: 93591

runBlocking must never be called from a coroutine in the first place. Since we are violating this contract by putting it in a suspend function, any explanation we have for why it's behaving the way it is might be different on different platforms or in the future.

Aside from that, blocking code should never be called in a coroutine unless you are in a CoroutineContext that uses a dispatcher that can handle it, like Dispatchers.IO.

That said, the reason this is happening is that coroutineScope suspends until all of its children coroutines finish, and then you are logging after it returns. runBlocking behaves similarly, but you are logging from inside the block without waiting.

If you wanted to wait for all the coroutines launched before logging, you need to join() each of them. You can put them in a list and call joinAll() on it. For example:

(1..1000).map {
    launch {
        delay(10)
        num += 1
    }
}.joinAll()

But again, runBlocking should never be called in a coroutine. I'm only describing how you would do it if you were using runBlocking from outside a coroutine for its intended purpose, to bridge between non-coroutine and coroutine code.

Upvotes: 2

broot
broot

Reputation: 28332

Existing answers focus on what we should not do, but I think they miss the point, so the main reason why we see the difference.

Both coroutineScope() and runBlocking() guarantee that after exiting the code block all coroutines inside already finished running. But for some reason, I don't know if intentional or not, you wrote both cases differently. In coroutine() example you put println() below coroutineScope() block, so it is guaranteed to run after all children. On the other hand, in runBlocked() you put println() inside runBlocking(), so it runs concurrently to children. Just rewrite your runBlocked() in a similar way to coroutine(), so put println() below runBlocking() and you will see 1000, as you expected.

Another difference between both examples is that by default runBlocking() runs using a single thread while coroutineScope() could use many of them. For this reason coroutineScope() produces a random value which is a result of unsafe sharing of a mutable state. runBlocking() is more predictable. It always produces 0 if println() is inside it, because println() runs before any children. Or it always produces 1000 if println() is below runBlocking(), because children are in fact running one at a time, they don't modify the value in parallel.

Upvotes: 3

Louis Wasserman
Louis Wasserman

Reputation: 198033

You should never call runBlocking from inside a suspend fun.

In any event, you don't wait for the launched coroutines to finish before you return a value from runBlocking, but your use of coroutineScope forces the launched coroutines in that function to complete before you get to the return.

Upvotes: 2

Related Questions