article banner

What is CoroutineContext and how does it work?

This is a chapter from the book Kotlin Coroutines. You can find it on LeanPub or Amazon.

If you take a look at 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, so let's check out its definition:

public interface CoroutineScope { public val coroutineContext: CoroutineContext }

It seems to be just a wrapper around CoroutineContext. Now, let's recall how Continuation is defined.

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

Continuation also contains CoroutineContext, which is used by the most important Kotlin coroutine elements. This must be a really important concept, so what is it?

CoroutineContext interface

CoroutineContext is an interface that represents 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. Each CoroutineContext.Element is also a CoroutineContext, which can also represent multiple elements added together. This means that CoroutineContext implements the Composite design pattern.

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 val ctx: CoroutineContext = name + job } //sampleEnd

CoroutineContext is implemented as a composite for its simpler creation. Wherever a CoroutineContext is expected, you can pass a single element without wrapping it in a collection. This is because a single element is also a CoroutineContext. But when you need to pass more than one element, you can add elements together, and the resulting context will contain all of them.

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

Finding elements in CoroutineContext

Since CoroutineContext represents a collection, we can find an element with a concrete key using get. Another option is to use square brackets, because the get method in Kotlin is an operator and can be invoked using square brackets instead of an explicit function call. The result of this operation is a nullable element of the type that is associated with the key. The operation works in a similar way as getting an element from a Map: if 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] // or ctx.get(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; however, contexts like Job or CoroutineName are part of the kotlinx.coroutines library, so they need to be imported from kotlinx.coroutines.

To find a CoroutineName, we use just CoroutineName. This is not a type or a class: it is a companion object. It is a feature of Kotlin that the 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 common practice in the kotlinx.coroutines library to use companion objects as keys to elements with the same name. This makes their names easier to remember2. A key might point to a class (like CoroutineName) or to an interface (like Job) that is implemented by many classes with the same key (like Job and SupervisorJob).

interface Job : CoroutineContext.Element { companion object Key : CoroutineContext.Key<Job> // ... }

Adding contexts

What makes CoroutineContext truly useful is its ability to merge two contexts 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

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

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 an empty context. Such a context by itself returns no elements; if we add it to another context, it behaves exactly like this other context.

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 their key using the minusKey function.

The minus operator is not overloaded for CoroutineContext. I believe this is because its meaning would not be clear enough, as explained in Effective Kotlin Item 11: An operator's 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 a context, we can use the fold method, which is similar to fold for 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 in.
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. By default, the parent passes its context to the child, which is one of the effects of the parent-child relationship. We say that a child inherits its context from its parent.

import kotlinx.coroutines.* //sampleStart fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } fun main(): Unit = 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. This context overrides the one from the parent.

import kotlinx.coroutines.* fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } //sampleStart fun main(): Unit = 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 new elements always replace old ones with the same key, the child context always overrides 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 they only set CoroutineId when the application is in debug mode (used for displaying coroutine ids in debug mode).

There is one special context called Job, which keeps the state of the current coroutine. It is the only context that is not inherited because each coroutine must have its own job. We will discuss it in detail in the Job and coroutine lifecycle chapter.

Accessing context in a suspending function

CoroutineScope has a coroutineContext property that can be used to access the context. But what if we are in a regular suspending function? As you might remember from the Coroutines under the hood chapter, context is referenced by continuations, which are passed to each suspending function, therefore it is possible to access a parent's context in a suspending function. To do this, we use the coroutineContext property, which is available in every suspending scope.

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

Coroutine scope functions capture the context from the scope they are called in, so their scope also inherits the context from the parent scope. If coroutineScope is called in a suspending function, the coroutine it creates is a direct child of the coroutine that started this function, therefore it inherits the context from the parent coroutine.

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

Changing context in suspending functions

If you want to modify a context for a suspend function, you can use withContext, which behaves like coroutineScope but changes the context for the coroutine it creates. It is a suspending function, so it can be used in other suspending functions.

suspend fun readSave() = withContext(Dispatchers.IO) { val file = File("save.txt") file.readText() }

The only difference between coroutineScope and withContext is that withContext changes the context for the coroutine it creates, so withContext(EmptyCoroutineContext) behaves just like coroutineScope.

withContext is often used to define a context that should be specific to a given operation. Remember that context is propagated automatically, so whatever is set in the parent coroutine will be available in the child coroutine.

import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.withContext import kotlin.coroutines.coroutineContext suspend fun printName() { println(coroutineContext[CoroutineName]?.name) } suspend fun main() { printName() // null a() } suspend fun a() = withContext(CoroutineName("a")) { printName() // a b() } suspend fun b() = withContext(CoroutineName("b")) { printName() // b }

Creating our own context

It is not a common need, but we can create our own coroutine context pretty easily. To do this, the easiest way is to create a class that implements the CoroutineContext.Element interface. Such a class needs a property key of type CoroutineContext.Key<*>, which will be used as the key that identifies this context. The common practice is to use this class's companion object as a key. This is how a 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 context with the same key. To see this in practice, below you can see an example context that is designed to print consecutive numbers.

import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext 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> } suspend fun 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 }

Custom contexts can be used to pass data or to define some behavior that should be specific for a specific coroutine.

Coroutines and thread elements

Kotlin Coroutines are not associated with concrete threads. A coroutine might start on one thread, get suspended, and then be resumed on another thread. This fact generates a problem for Java tools that associate data with threads, like ThreadLocal in Java stdlib, or SecurityContext in Spring Boot. If you use such tools, Kotlin Coroutines offer a special kind of context. All contexts that extend ThreadContextElement are installed into the thread context every time the coroutine with this element in the context is resumed on a thread.

import kotlinx.coroutines.ThreadContextElement import kotlinx.coroutines.runBlocking import kotlin.coroutines.CoroutineContext class CoroutineName( val name: String ) : ThreadContextElement<String> { companion object Key : CoroutineContext.Key<CoroutineName> override val key: CoroutineContext.Key<CoroutineName> = Key // invoked before coroutine is resumed on current thread override fun updateThreadContext( context: CoroutineContext ): String { val previousName = Thread.currentThread().name Thread.currentThread().name = "$previousName # $name" return previousName } // invoked after coroutine has suspended on current thread override fun restoreThreadContext( context: CoroutineContext, oldState: String ) { Thread.currentThread().name = oldState } } fun main(): Unit = runBlocking(CoroutineName("MyCoroutine")) { println("Running in thread: ${Thread.currentThread().name}") } // Running in thread: main # MyCoroutine

This is how ThreadContextElement can be used to associate a coroutine with SecurityContext, which is used by some Spring Boot applications to implicitly keep request-specific data that are used for security checks. This context needs to be used when a coroutine is launched3.

class SecurityCoroutineContext( private val securityContext: SecurityContext = SecurityContextHolder.getContext() ) : ThreadContextElement<SecurityContext?> { companion object Key : Key<SecurityCoroutineContext> override val key: Key<SecurityCoroutineContext> = Key override fun updateThreadContext( context: CoroutineContext ): SecurityContext? { val previousSecurityContext = SecurityContextHolder .getContext() SecurityContextHolder.setContext(securityContext) return previousSecurityContext .takeIf { it.authentication != null } } override fun restoreThreadContext( context: CoroutineContext, oldState: SecurityContext? ) { if (oldState == null) { SecurityContextHolder.clearContext() } else { SecurityContextHolder.setContext(oldState) } } }

ThreadContextElement is also used when we want to use ThreadLocal in a coroutine. This cannot be done directly because ThreadLocal is associated with a thread, and a coroutine might be resumed on a different thread. This is why Kotlin Coroutines provide the asContextElement method, which transforms ThreadLocal into a context element. This element is installed into the thread context every time the coroutine with this element in the context is resumed on a thread. Such a thread-local becomes a coroutine-local.

import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asContextElement import kotlinx.coroutines.withContext val id = ThreadLocal<String?>() suspend fun main(): Unit = withContext(Dispatchers.Default) { println(id.get()) // null withContext(id.asContextElement("A")) { println(id.get()) // A withContext(Dispatchers.IO) { println(id.get()) // A } } println(id.get()) // null }

Another example is MDCContext from the kotlinx-coroutines-slf4j library, which is used to propagate Mapped Diagnostic Context (MDC) from the SLF4J library.

launch(MDCContext()) { MDC.put("key", "value") // This update will be captured withContext(MDCContext()) { delay(1000) println(MDC.get("key")) // This will print "value" } }

These tools were designed for applications that block threads. I do not recommend using them in Kotlin Coroutines unless you need to do so for compatibility with legacy libraries or parts of an application that have not yet been migrated to coroutines. All these can be replaced with much simpler and lighter coroutine contexts.

import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext class MyId(val id: String):AbstractCoroutineContextElement(MyId) { companion object Key : CoroutineContext.Key<MyId> } suspend fun getId() = coroutineContext[MyId]?.id suspend fun main(): Unit = withContext(Dispatchers.Default) { val id = MyId("A") println(getId()) // null withContext(id) { println(getId()) // A withContext(Dispatchers.IO) { println(getId()) // A } } println(getId()) // null }

Summary

CoroutineContext is conceptually similar to a map or a set collection. It is an indexed set of Element instances, where each Element is also a CoroutineContext in which every element has a unique Key that is used to identify it. Therefore, CoroutineContext is just a universal way to group and pass objects to coroutines. These objects are kept by the coroutines and can determine how these coroutines should run (what their state is, on which thread, etc). The context is inherited from the parent coroutine, but it can be overridden in the child coroutine. It propagates through the whole coroutine hierarchy, so it is available in all child coroutines. In the next chapters, we will discuss dispatchers, which are the most often explicitly used contexts.

1:

Let's clear up the nomenclature. launch is an extension function on CoroutineScope, so CoroutineScope is its receiver type. The extension function's receiver is the object we reference with this.

2:

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