Search code examples
kotlinkotlin-coroutines

Difference between starting coroutine with launch or with coroutineScope


It seems I can start a coroutine using a number of mechanisms. Two of them are coroutineScope and launch but reading the Kotlin coroutine docs I am unclear what the difference is and when I would use one over the other

fun main() {
    println("Main block  ${Thread.currentThread().name}")
    runBlocking {
        coroutineScope {
            println("Coroutine scope  ${Thread.currentThread().name}")
        }
        launch {
            println("Launch  ${Thread.currentThread().name}")
        }
    }
}

I ran the above and can see that both start a new coroutine. What is the difference?


Solution

  • A CoroutineScope is an organisational thing - it lets you group coroutines together to enforce structured concurrency. Basically, it allows you to control the lifetime of everything within the scope (including coroutines launched within other coroutines) and handles things like propagating results and errors, and cancelling everything within a scope. More info here

    All the coroutine builder functions (including launch) run inside a CoroutineScope (or on it as a receiver, if you want to look at it that way):

    fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext, 
        start: CoroutineStart = CoroutineStart.DEFAULT, 
        block: suspend CoroutineScope.() -> Unit
    ): Job
    

    So the launch call in your example is running inside the current CoroutineScope, which has been provided by the runBlocking builder:

    expect fun <T> runBlocking(
        context: CoroutineContext = EmptyCoroutineContext,
        block: suspend CoroutineScope.() -> T
    ): T
    

    See how the lambda you're passing is actually a CoroutineScope.() -> T? It runs with a CoroutineScope as the receiver - that's what you're calling launch on. You can't call it from just anywhere!

    So when you create a CoroutineScope in your example, you're nesting one inside this CoroutineScope that runBlocking is providing. runBlocking won't allow the rest of the code that follows it to continue (i.e. it blocks execution) until everything in its CoroutineScope has completed. Creating your own CoroutineScope inside there lets you define a subset of coroutines that you can control - you can cancel them, but if there are still other coroutines running in the runBlocking scope, it will keep blocking until they're done as well.


    Like Louis says in the other answer, you're not actually creating any coroutines inside the scope you've created. The code is just running in the coroutine that runBlocking started to run the block of code you passed. Check this out:

    fun main() {
        println("Main")
        runBlocking {
            coroutineScope {
                delay(1000)
                println("I'm in a coroutine scope")
            }
            launch {
                delay(500)
                println("First new coroutine")
            }
            launch {
                delay(50)
                println("Second new coroutine")
            }
        }
    }
    
    Main
    I'm in a coroutine scope
    Second new coroutine
    First new coroutine
    

    Code within a single coroutine executes sequentially - so what happens is

    • You start in main
    • You enter the runBlocking block's coroutine
    • You create a CoroutineScope which only affects stuff you launch (etc) on it
    • You delay, then print a line
    • You hit the first launch and create a new coroutine. This starts off by delaying 500 ms, but it's running on its own now - different coroutines run concurrently
    • You move onto the next line, and launch the second coroutine. That runs on its own too, and immediately delays 50ms. There are 3 coroutines running now
    • You've finished executing the code in the block - this coroutine is done. However, there are two running coroutines that were also started on this CoroutineScope, so runBlocking waits for those to finish.
    • The second coroutine with the shortest delay prints first, then finishes
    • The first coroutine finishes last, because even though it was started earlier, they all run independently
    • Now everything inside the runBlocking CoroutineScope is done, it can finish and allow execution to contine in main()

    If you launched a coroutine inside that scope you created, it would all work the same way - it's just you'd have fine-grained control over the stuff in there in particular, and you could specifically work with the coroutines inside that nested scope without affecting things in the outer scope. All that matters is once that inner scope has completed, it can inform the outer scope that it's all done, that kind of thing