Can anyone explain the difference between them? I think scope provides a reference(e.g. Job) to cancel them and context provides a reference to underlying thread. Is that so?
3 Answers
Yes, in principle you are right, here more details.
Scope
- a coroutine must run in a scope
- it's a way to keep track of all coroutines that run in it
- all (cooperative) coroutines can be cancelled via their scope
- scopes get uncaught exceptions
- they're a way to bind coroutines to an application specific lifecycle (e.g.
viewModelScope
in Android) to avoid leaking
Context
The coroutine context is a set of various elements. The main elements are the Job of the coroutine, which we've seen before, and its dispatcher [...]. (Source)
In case you specify a dispatcher there are four options which basically determine on which thread the coroutines will run:
Dispatchers.Default
- for CPU intense work (e.g. sorting a big list)Dispatchers.Main
- what this will be depends on what you've added to your programs runtime dependencies (e.g.kotlinx-coroutines-android
, for the UI thread in Android)Dispatchers.Unconfined
- runs coroutines unconfined on no specific threadDispatchers.IO
- for heavy IO work (e.g. long-running database queries)
The following example brings both scope and context together. It creates a new scope in which the coroutines will run (if not changed) on a thread designated for IO work and cancels them via their scope.
val scope = CoroutineScope(context = Dispatchers.IO)
val job = scope.launch {
val result = suspendFunc1()
suspendFunc2(result)
}
// ...
scope.cancel() // suspendFunc1() and suspendFunc2() will be cancelled
They are indeed closely related. You might say that CoroutineScope
formalizes the way the CoroutineContext
is inherited.
CoroutineScope
has no data on its own, it just holds a CoroutineContext
. Its key role is as the implicit receiver of the block you pass to launch
, async
etc.
See this example:
runBlocking {
val scope0 = this
// scope0 is the top-level coroutine scope.
scope0.launch {
val scope1 = this
// scope1 inherits its context from scope0. It replaces the Job field
// with its own job, which is a child of the job in scope0.
// It retains the Dispatcher field so the launched coroutine uses
// the dispatcher created by runBlocking.
scope1.launch {
val scope2 = this
// scope2 inherits from scope1
}
}
}
You can see how the CoroutineScope
mediates the inheritance of coroutine contexts. If you cancel the job in scope1
, this will propagate to scope2
and will cancel the launch
ed job as well.
Note the key syntactical feature: I explicitly wrote scope0.launch
, but had I written just launch
, it would implicitly mean exactly the same thing. This is how CoroutineScope
helps to "automatically" propagate the scope.
CoroutineScope
has-a CoroutineContext
.
For example if you have:
runBlocking { // defines coroutineScope
launch(Dispatchers.Default) { //inherits coroutineScope but changes context
}
}
runBlocking
defines a CoroutineScope
(learn about it here) which launch
inherits. The context is being overridden by explicitly specifying a dispatcher here. If you look at the definition of launch
, you can see that it takes an optional CoroutineContext
:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
...
)
Another part of the context would be the coroutine's name:
launch(CoroutineName("launchMe") + Dispatchers.Default) {
println("")
}
-
5If you re-read your answer, you can really see you did not explain anything about the difference– FaridCommented Feb 1, 2023 at 10:35