What is the CoroutineContext?
If you can’t explain it simply, you don’t understand it well enough. — Albert Einstein
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.
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 inCoroutineScope
.
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.
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.