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

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Roman Elizarov
Roman Elizarov

Written by Roman Elizarov

Project Lead for the Kotlin Programming Language @JetBrains

Responses (15)

What are your thoughts?