Coroutine Context and Scope

Roman Elizarov
4 min readMar 9, 2019

--

Different uses of physically near-identical things are usually accompanied by giving those things different names to emphasize the intended purpose. Depending on the use, seamen have a dozen or more words for a rope though it might materially be the same thing. (Wikipedia on Hindley-Milner type system)

Every coroutine in Kotlin has a context that is represented by an instance of CoroutineContext interface. A context is a set of elements and current coroutine context is available via coroutineContext property:

Coroutine context is immutable, but you can add elements to a context using plus operator, just like you add elements to a set, producing a new context instance:

A coroutine itself is represented by a Job. It is responsible for coroutine’s lifecycle, cancellation, and parent-child relations. A current job can be retrieved from a current coroutine’s context:

There is also an interface called CoroutineScope that consists of a sole property — val coroutineContext: CoroutineContext. It has nothing else but a context. So, why it exists and how is it different from a context itself? The difference between a context and a scope is in their intended purpose.

A coroutine is typically launched using launch coroutine builder:

fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
// ...
): Job

It is defined as extension function on CoroutineScope and takes a CoroutineContext as parameter, so it actually takes two coroutine contexts (since a scope is just a reference to a context). What does it do with them? It merges them using plus operator, producing a set-union of their elements, so that the elements in context parameter are taking precedence over the elements from the scope. The resulting context is used to start a new coroutine, but it is not the context of the new coroutine — is the parent context of the new coroutine. The new coroutine creates its own child Job instance (using a job from this context as its parent) and defines its child context as a parent context plus its job:

The intended purpose of CoroutineScope receiver in launch and in all the other coroutine builders is to reference a scope in which new coroutine is launched. By convention, a context in CoroutineScope contains a Job that is going to become a parent of new coroutine (with the exception of GlobalScope that you should avoid anyway¹).

On the other hand, the intended purpose of context: CoroutineContext parameter in launch is to provide additional context elements to override elements that would be otherwise inherited from a parent scope. For example:

By convention, we do not usually pass a Job in a context parameter to launch, since that breaks parent-child relation, unless we explicitly want to break it using a NonCancellable job, for example.

Notice, that a block of code inside launch is defined with CoroutineScope as its receiver:

fun CoroutineScope.launch(
// ...
block: suspend CoroutineScope.() -> Unit
): Job

By convention which is followed by all coroutine builders, this scope’s coroutineContext property is the same as the context of the coroutine that is running inside this block:

This way, when we see an unqualified coroutineContext reference in code, there is no confusion between the correspondingly named top-level property and a scope’s property, since they are the same at all times by design.

IntelliJ IDEA handily marks the block of code inside coroutine builders with this: CoroutineScope hint which lets us immediately distinguish regular code blocks from the blocks with a different context. Moreover, this new CoroutineScope always has a new Job in its context. So, when you see launch { … } in the source code without an explicit receiver you can quickly tell what scope it is launched in by looking for the outer block marked as this: CoroutineScope.

Since the context and the scope are materially the same thing, we could have launched a coroutine without having access to the scope and without using GlobalScope simply by wrapping the current coroutineContext into the instance of CoroutineScope as shown in the following function:

Do not do this! It makes the scope in which the coroutine is launched opaque and implicit, capturing some outer Job to launch a new coroutine without explicitly announcing it in the function signature. A coroutine is a piece of work that is concurrent with the rest of your code and its launch has to be explicit².

If you need to launch a coroutine that keeps running after your function returns, then make your function an extension of CoroutineScope or pass scope: CoroutineScope as parameter to make your intent clear in your function signature. Do not make these functions suspending:

Suspending functions, on the other hand, are designed to be non-blocking and should not have side-effects of launching any concurrent work. Suspending functions can and should wait for all their work to complete before returning to the caller³.

--

--

Roman Elizarov
Roman Elizarov

Written by Roman Elizarov

Project Lead for the Kotlin Programming Language @JetBrains

Responses (16)