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
.
The receiver and the last argument's receiver are of type CoroutineScope
1. This CoroutineScope
seems to be an important concept, so let's check out its definition:
It seems to be just a wrapper around CoroutineContext
. Now, let's recall how Continuation
is defined.
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.
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.
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.
CoroutineContext
is part of the built-in support for Kotlin coroutines, so it is imported fromkotlin.coroutines
; however, contexts likeJob
orCoroutineName
are part of the kotlinx.coroutines library, so they need to be imported fromkotlinx.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]
.
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
).
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.
Just like in a map, the new element replaces the previous one when another element with the same key is added.
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.
Subtracting elements
Elements can also be removed from a context by their key using the minusKey
function.
The
minus
operator is not overloaded forCoroutineContext
. 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.
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.
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.
Each child might have a specific context defined in the argument. This context overrides the one from the parent.
A simplified formula to calculate a coroutine context is:
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.
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.
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.
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.
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:
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.
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.
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.
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.
Another example is MDCContext
from the kotlinx-coroutines-slf4j
library, which is used to propagate Mapped Diagnostic Context (MDC) from the SLF4J library.
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.
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.
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
.
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.
Inspired by the article Propagating the Spring SecurityContext to your Kotlin Coroutines by Riccardo Lippolis.