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?
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
runBlocking
block's coroutineCoroutineScope
which only affects stuff you launch
(etc) on itlaunch
and create a new coroutine. This starts off by delaying 500 ms, but it's running on its own now - different coroutines run concurrentlylaunch
the second coroutine. That runs on its own too, and immediately delays 50ms. There are 3 coroutines running nowCoroutineScope
, so runBlocking
waits for those to finish.runBlocking
CoroutineScope
is done, it can finish and allow execution to contine in main()
If you launch
ed 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