Kotlin: Coroutines scope vs Coroutine context

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?


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 launched 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.


Yes, in principle you are right, here more details.

Scope

  • a coroutine must run in a scope
  • it is 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 are a way to bind coroutines to an application specific lifecycle (e.g. viewModelScope in Android) to avoid leaking

Context

The context determines on which thread the coroutines will run. There are four options:

  • 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 thread
  • Dispatchers.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

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("")
}