What is CoroutineContext and how does it work?

This is a chapter from the book Kotlin Coroutines. You can find Early Access on LeanPub.

If you take a look at the coroutine builders’ definitions, you will see that their first parameter is of type CoroutineContext.

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { ... }

The receiver and the last argument’s receiver are of type CoroutineScope1. This CoroutineScope seems to be an important concept. Let's check out its definition:

public interface CoroutineScope { public val coroutineContext: CoroutineContext }

It seems to be just a wrapper around CoroutineContext. Then you might remind yourself how Continuation is defined.

public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) }

It contains CoroutineContext as well. This type is used by the most important Kotlin coroutine elements. It must be a really important concept. So what is it?

CoroutineContext interface

CoroutineContext is an interface, representing an element or a collection of elements. It is conceptually similar to a map or a set collection. It is an indexed set of Element instances like Job, CoroutineName, CouroutineDispatcher, etc. The unusual thing is that each Element is a CoroutineContext as well. So it is like every element in the collection, is a collection by itself. This is done this way, to allow convenient context specification and modification, as in the below example (adding contexts and setting coroutine builder context will be explained later).

launch(CoroutineName("Name1")) { ... } launch(CoroutineName("Name2") + Job()) { ... }

Every element in this set has a unique Key, that is used to identify it. These keys are compared by reference.

For example CoroutineName or Job implement CoroutineContext.Element, which implements CoroutineContext interface.

import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlin.coroutines.CoroutineContext //sampleStart fun main() { val name: CoroutineName = CoroutineName("A name") val element: CoroutineContext.Element = name val context: CoroutineContext = element val job: Job = Job() val jobElement: CoroutineContext.Element = job val jobContext: CoroutineContext = jobElement } //sampleEnd

Same with SupervisorJob, CoroutineExceptionHandler and dispatchers from the Dispatchers object. Those are the most important coroutine contexts. They will be explained in the next chapters.

Finding elements in CoroutineContext

Since CoroutineContext is like a collection, we can find an element with a concrete key using getter. In Kotlin get method is an operator and can be invoked using square brackets instead. Just like in a map, when an element is in the context, it will be returned. If it is not, null will be returned instead.

import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlin.coroutines.CoroutineContext //sampleStart fun main() { val ctx: CoroutineContext = CoroutineName("A name") val coroutineName: CoroutineName? = ctx[CoroutineName] println(coroutineName?.name) // A name val job: Job? = ctx[Job] // or ctx.get(Job) println(job) // null } //sampleEnd

CoroutineContext is part of the built-in support for Kotlin coroutines, so it is imported from kotlin.coroutines, when contexts like Job or CoroutineName are part of the kotlinx.coroutines library, so they needs to be imported from kotlinx.coroutines.

To find a CoroutineName, we used just CoroutineName. This is not a type, or a class, this is the companion object. It is a Kotlin feature, that a name of a class used by itself acts as a reference to its companion object, so ctx[CoroutineName] is just a shortcut to ctx[CoroutineName.Key].

data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) { override fun toString(): String = "CoroutineName($name)" companion object Key : CoroutineContext.Key<CoroutineName> }

It is a common practice in kotlinx.coroutines library, to use companion objects as keys to the elements with the same name. It makes it easier to remember2. The element might be a class (like CoroutineName) or it might be an interface (like Job) implemented by many classes acts as elements with the same key (like Job and SupervisorJob).

Adding contexts

What makes CoroutineContext truly useful is the ability to merge two of them together.

When two elements with different keys are added, the resulting context responds to both keys.

import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlin.coroutines.CoroutineContext //sampleStart fun main() { val ctx1: CoroutineContext = CoroutineName("Name1") println(ctx1[CoroutineName]?.name) // Name1 println(ctx1[Job]?.isActive) // null val ctx2: CoroutineContext = Job() println(ctx2[CoroutineName]?.name) // null println(ctx2[Job]?.isActive) // true, because "Active" // is the default state of a job created this way val ctx3 = ctx1 + ctx2 println(ctx3[CoroutineName]?.name) // Name1 println(ctx3[Job]?.isActive) // true } //sampleEnd

When another element with the same key is added, just like in a map, the new element replaces the previous one.

import kotlinx.coroutines.CoroutineName import kotlin.coroutines.CoroutineContext //sampleStart fun main() { val ctx1: CoroutineContext = CoroutineName("Name1") println(ctx1[CoroutineName]?.name) // Name1 val ctx2: CoroutineContext = CoroutineName("Name2") println(ctx2[CoroutineName]?.name) // Name2 val ctx3 = ctx1 + ctx2 println(ctx3[CoroutineName]?.name) // Name2 } //sampleEnd

Empty coroutine context

Since CoroutineContext is like a collection, we also have one that is empty. Such context by itself returns no elements and does not modify another context if added.

import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext //sampleStart fun main() { val empty: CoroutineContext = EmptyCoroutineContext println(empty[CoroutineName]) // null println(empty[Job]) // null val ctxName = empty + CoroutineName("Name1") + empty println(ctxName[CoroutineName]) // CoroutineName(Name1) } //sampleEnd

Subtracting elements

Elements can also be removed from a context by key using the minusKey function.

Operator minus is not overloaded for CoroutineContext. I believe it is because its meaning would not be clear enough, as explained in the Effective Kotlin Item 12: Operator meaning should be consistent with its function name.

import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job //sampleStart fun main() { val ctx = CoroutineName("Name1") + Job() println(ctx[CoroutineName]?.name) // Name1 println(ctx[Job]?.isActive) // true val ctx2 = ctx.minusKey(CoroutineName) println(ctx2[CoroutineName]?.name) // null println(ctx2[Job]?.isActive) // true val ctx3 = (ctx + CoroutineName("Name2")) .minusKey(CoroutineName) println(ctx3[CoroutineName]?.name) // null println(ctx3[Job]?.isActive) // true } //sampleEnd

Folding context

If we need to do something for each element in the context, we can use the fold method for that. It is similar to fold on other collections - it takes:

  • an initial accumulator value,
  • an operation to produce the next state of the accumulator, based on the current state, and the element it is currently invoked on.
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlin.coroutines.CoroutineContext //sampleStart fun main() { val ctx = CoroutineName("Name1") + Job() ctx.fold("") { acc, element -> "$acc$element " } .also(::println) // CoroutineName(Name1) JobImpl{Active}@dbab622e val empty = emptyList<CoroutineContext>() ctx.fold(empty) { acc, element -> acc + element } .joinToString() .also(::println) // CoroutineName(Name1), JobImpl{Active}@dbab622e } //sampleEnd

Coroutine context and builders

So CoroutineContext is just a way to hold and pass data. Since some of its elements are mutable (Job for instance), we can let them communicate with each other. For instance, such communication happens between a parent and a child coroutine. By default, the parent passes its context to the child, which is one of the parent-child relationship effects. We say that the child inherits context from its parent.

import kotlinx.coroutines.* //sampleStart fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } fun main() = runBlocking(CoroutineName("main")) { log("Started") // [main] Started val v1 = async { delay(500) log("Running async") // [main] Running async 42 } launch { delay(1000) log("Running launch") // [main] Running launch } log("The answer is ${v1.await()}") // [main] The answer is 42 } //sampleEnd

Each child might have a specific context defined in the argument.

import kotlinx.coroutines.* fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } //sampleStart fun main() = runBlocking(CoroutineName("main")) { log("Started") // [main] Started val v1 = async(CoroutineName("c1")) { delay(500) log("Running async") // [c1] Running async 42 } launch(CoroutineName("c2")) { delay(1000) log("Running launch") // [c2] Running launch } log("The answer is ${v1.await()}") // [main] The answer is 42 } //sampleEnd

A simplified formula to calculate a coroutine context is:

defaultContext + parentContext + childContext

Since the new elements with the same key always replace the old ones, the child context always overrides the elements with the same key from the parent context. The defaults are used only for keys that are not specified anywhere else. Currently, the defaults only set Dispatchers.Default when no ContinuationInterceptor is set, and CoroutineId when the application is in debug mode.

Creating our own context

It is not a common need, but we might create our own coroutine context pretty easily. To do that, the easiest way is to create a class that implements the CoroutineContext.Element interface. Such class needs a property key of type CoroutineContext.Key<*>. This key will be used as a key identifying this context. The common practice is to use this class companion object as a key. This is how a very simple coroutine context can be implemented:

class MyCustomContext : CoroutineContext.Element { override val key: CoroutineContext.Key<*> = Key companion object Key : CoroutineContext.Key<MyCustomContext> }

Such a context will behave a lot like CoroutineName - it will propagate from parent to child, but any children will be able to override it with a different one of the same type. To see it in practice, below you can see how an example context printing next numbers works in action.

import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext class CounterContext( private val name: String ) : CoroutineContext.Element { override val key: CoroutineContext.Key<*> = Key private var nextNumber = 0 fun printNext() { println("$name: $nextNumber") nextNumber++ } companion object Key : CoroutineContext.Key<CounterContext> } fun CoroutineScope.printNext() { coroutineContext[CounterContext]?.printNext() } suspend fun main(): Unit = withContext(CounterContext("outer")) { printNext() // outer: 0 launch { printNext() // outer: 1 launch { printNext() // outer: 2 } launch(CounterContext("inner")) { printNext() // inner: 0 printNext() // inner: 1 launch { printNext() // inner: 2 } } } printNext() // outer: 3 }

I have seen custom contexts in use as a kind of dependency injection - to easily inject different values on production and different on tests. Although I don't think it will become standard practice.

import kotlinx.coroutines.withContext import java.util.* import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext import kotlin.test.assertEquals data class User(val id: String, val name: String) abstract class UuidProviderContext : CoroutineContext.Element { abstract fun nextUuid(): String override val key: CoroutineContext.Key<*> = Key companion object Key : CoroutineContext.Key<UuidProviderContext> } class RealUuidProviderContext : UuidProviderContext() { override fun nextUuid(): String = UUID.randomUUID().toString() } class FakeUuidProviderContext( private val fakeUuid: String ) : UuidProviderContext() { override fun nextUuid(): String = fakeUuid } suspend fun nextUuid(): String = checkNotNull(coroutineContext[UuidProviderContext]) { "UuidProviderContext not present" } .nextUuid() // function under test suspend fun makeUser(name: String) = User( id = nextUuid(), name = name ) suspend fun main(): Unit { // production case withContext(RealUuidProviderContext()) { println(makeUser("Michał")) // e.g. User(id=d260482a-..., name=Michał) } // test case withContext(FakeUuidProviderContext("FAKE_UUID")) { val user = makeUser("Michał") println(user) // User(id=FAKE_UUID, name=Michał) assertEquals(User("FAKE_UUID", "Michał"), user) } }

Accessing

We do not really need to have CoroutineScope to access context. As you might remember from the chapter Coroutines under the hood, context is referenced by continuations, which are passed to each suspending function. The suspending function context can be referenced using coroutineContext suspending extension property.

import kotlinx.coroutines.* import kotlin.coroutines.coroutineContext suspend fun printName() { println(coroutineContext[CoroutineName]?.name) } suspend fun main() = withContext(CoroutineName("Outer")) { printName() // Outer launch(CoroutineName("Inner")) { printName() // Inner } delay(10) printName() // Outer }

This also means that the printNext function from the CounterContext example can be implemented just as a suspending function (instead of as an extension on CoroutineScope).

suspend fun printNext() { coroutineContext[CounterContext]?.printNext() }

Summary

CoroutineContext is conceptually similar to a map or a set collection. It is an indexed set of Element instances, where each Element is a CoroutineContext as well. Every element in it has a unique Key, that is used to identify it. This way, CoroutineContext is just a universal way to group and pass objects to coroutines. Those objects are kept by the coroutines, and can determine how those coroutines should be running (what is their state, at what thread, etc). In the next chapters, we will discuss the most essential coroutine contexts in the Kotlin coroutines library.

1:

Let’s clear the nomenclature. launch is an extension function on CoroutineContext, so CoroutineContext is its receiver type. Extension function receiver is the object we use this to reference to.

2:

The below companion object is named Key. It is possible but changes little in terms of how it is used. The default companion object name is Companion, and so this name is used when we need to reference it using reflection or when we define an extension function on it. Here we use Key instead.