What is the CoroutineContext?

Ibrahim Yilmaz
5 min readDec 8, 2019

If you can’t explain it simply, you don’t understand it well enough. — Albert Einstein

Photo by RawFilm on Unsplash

What is exactly this CoroutineContext?

To give very simple question what exactly this CoroutineContext is, the first place that we should look kotlinlang.org.

It is written there:

Coroutines always execute in some context represented by a value of the CoroutineContext type, defined in the Kotlin standard library

We understand that the code in the coroutine is executed in a CoroutineContext. So we can say that CoroutineContext provide us required execution environment to run/execute our code asyncronously.

What does this execution environment have?

We can easily run coroutine by using launch/async . We can reach CoroutineContext in the functional scope of launch/async by using coroutineContext . So let’s print and see it.

Printing CoroutineContext in Coroutine

Output:

launch: [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Active}@5eb5c224[or BlockingCoroutine5eb5c224],BlockingEventLoop@53e25b76,DefaultDispatcher{In Android, we can see this}]

Let’s take deep look at what this output means:

CoroutineId:

It is only created for debugging purpose.

And here we can see one very important thing it is

val combined = couroutineContext + context

We will take a look at this deeply below.

StandaloneCoroutine:

As we have used launch method without Start parameter, StandaloneCoroutine is created.

And here we can see three major things to understand CoroutineContext better. First one is newContext creation. newCoroutineContext is created by using the given context. Second one is that new context is used in coroutine creation. And the last one is that our block is given to the created coroutine. After checking BlockingEventLoop, we will return this point again below.

We can see the answer of what :the block which is suspend function in CoroutineScope .

BlockingEventLoop:

This class is defined in EventLoop.kt

kotlinx-coroutines-core/jvm/src/EventLoop.kt

We can only see here thread When we search where createEventLoop method is defined. It is defined in:

kotlinx-coroutines-core/common/src/EventLoop.common.kt

eventLoop is called by related environment. You can see it for JVM

kotlinx-coroutines-core/jvm/src/Builders.kt

And it is coming from runBlocking because launch in our example runs on runBlocking block.

This explains the code above about related to coroutineContext copy:

val combined = couroutineContext + context

As runBlocking is the parent coroutine of the launch and its context is copied.

DefaultDispatcher:

It is defined in:

kotlinx-coroutines-core/jvm/src/Dispatchers.kt

This is mainly about where our coroutine works.

With the help of the ouput of very simple print of the CoroutineContext in launch , we understand what it is.

launch: [CoroutineId(2),"coroutine#2":StandaloneCoroutine{Active}@5eb5c224[or BlockingCoroutine5eb5c224],BlockingEventLoop@53e25b76,DefaultDispatcher{In Android, we can see this}]

Let’s take a look at CoroutineContext.kt :

We see Element class which is also CoroutineContext. When we search "CoroutineContext.Element" in the Kotlin/kotlinx.coroutines. We find:

  • ContinuationInterceptor -> interface
  • ThreadContextElement -> interface
  • Job -> interface
  • AbstractCoroutineContextElement -> abstract class

If we check their documentations and child classes/interfaces:

ContinuationInterceptor:

As it is experimental, we will skip this but

private class ContinuationInterceptorMigration(val interceptor: ExperimentalContinuationInterceptor) : ContinuationInterceptor {private class ExperimentalContinuationInterceptorMigration(val interceptor: ContinuationInterceptor) : ExperimentalContinuationInterceptor {

it is called in:

public fun CoroutineContext.toExperimentalCoroutineContext(): ExperimentalCoroutineContext {

ThreadContextElement:

CoroutineId: It is used debug purpose to follow coroutine by its id. we can find the details in:

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext):CoroutineContext {

ThreadLocalElement: It is used for creating thread local elements. As child Coroutine copy the context of the parent. We can pass parameter to child element with this.

* val myThreadLocal = ThreadLocal<String?>()
* ...
* println(myThreadLocal.get()) // Prints "null"
* launch(Dispatchers.Default + myThreadLocal.asContextElement(value = "foo")) {
* println(myThreadLocal.get()) // Prints "foo"
* withContext(Dispatchers.Main) {
* println(myThreadLocal.get()) // Prints "foo", but it's on UI thread
* }
* }
* println(myThreadLocal.get()) // Prints "null"

But the parent can’t reach the local variables of its children.

* The context element does not track modifications of the thread-local variable, for example:
*
* ```
* myThreadLocal.set("main")
* withContext(Dispatchers.Main) {
* println(myThreadLocal.get()) // Prints "main"
* myThreadLocal.set("UI")
* }
* println(myThreadLocal.get()) // Prints "main", not "UI"
* ```

Job:

I think the documentation of Job is very very clear. Job’s main responsibility is to manage coroutine cancellation.

* A background job. Conceptually, a job is a cancellable thing with a life-cycle that
* culminates in its completion.
*
* Jobs can be arranged into parent-child hierarchies where cancellation
* of a parent leads to immediate cancellation of all its [children]. Failure or cancellation of a child
* with an exception other than [CancellationException] immediately cancels its parent. This way, a parent
* can [cancel] its own children (including all their children recursively) without cancelling itself.
*
* The most basic instances of [Job] are created with [launch][CoroutineScope.launch] coroutine builder or with a
* `Job()` factory function. By default, a failure of any of the job's children leads to an immediate failure
* of its parent and cancellation of the rest of its children. This behavior can be customized using [SupervisorJob].
*
* Conceptually, an execution of the job does not produce a result value. Jobs are launched solely for their
* side-effects. See [Deferred] interface for a job that produces a result.

NonCancellable:

Its documentation also explains what it is very very clearly.

/**
* A non-cancelable job that is always [active][Job.isActive]. It is designed for [withContext] function
* to prevent cancellation of code blocks that need to be executed without cancellation.
*
* Use it like this:
* ```
* withContext(NonCancellable) {
* // this code will not be cancelled
* }
* ```
*/

Deferred:

Deferred is a Job with return value. We generally see this with async{ } . Its documentation is very very clear.

/**
* Creates a coroutine and returns its future result as an implementation of [Deferred].
...

CompletableJob:

It is a interface to add complete functionality to our Job. It is used in JobImpl.

JobImpl:

It is concerete implementation of Job concept that we generally use to orchestrate cancellation of our coroutines.

AbstractCoroutineContextElement:

It is base class of :

  • CoroutineName
  • CoroutineId
  • NonCancellable(Job)
  • CoroutineDispatcher
  • CoroutineId

CoroutineName:

We can specify the name of coroutine for debugging purpose.

CoroutineDispatcher:

As we talked about it before, we use this to specify where the coroutine will work. There are 4 dispatchers, which are Default, Main, IO and Unconfined.

Default: It is recommended to use CPU intensive works such as very costly mathematical computations.

IO: It is recommended to used IO Intensive works such as Network Call, File IO or socket IO

Main: It is recommended to use Main thread operations such as updating UI.

Unconfined: It is not confined for any use-case. It is recommend to use Testing purpose.

To sum up, we can clearly see that CoroutineContext is environment on which our Coroutines run.

Simply Coroutine Context

I hope that I explain clearly what CoroutineContext is.

Please comment If there is something not clear. In upcoming posts, I will try to mention about Coroutine cancellation and exceptions.

--

--