1. Introduction
In this tutorial, we’ll learn about the CoroutineContext and then continue with dispatchers as one of the important elements of the CoroutineContext.
2. What Is a Coroutine?
Coroutines are subroutines or programs that allow for cooperative multitasking. Therefore, coroutines can be suspended or resumed, or they can yield to another coroutine. In Kotlin, the suspend keyword before the function means that it suspends the caller and can be called only within a coroutine.
We have several coroutine builder functions: launch and async, extensions of CoroutineScope, and runBlocking.
3. Coroutine Context
Every coroutine has an associated CoroutineContext, which is an indexed set of Elements. So, what is an indexed set? It is a mixture of a set and a map, or in other words, it’s a set with a unique key for each of the elements. Also, CoroutineContext#get is remarkable as it provides type-safety in the lookup of heterogeneous elements:
public operator fun <E : Element> get(key: Key<E>): E?
All of the coroutine classes implement CoroutineScope and have the property coroutineContext. Therefore, we can access coroutineContext in the coroutine block:
runBlocking {
Assertions.assertNotNull(coroutineContext)
}
And we can read an element of a coroutineContext:
runBlocking {
Assertions.assertNotNull(coroutineContext[Job])
}
3.1. How to Manipulate a Coroutine Context?
CoroutineContext is immutable, but we can have a new context by adding an element, removing one, or merging two existing contexts. Also, a context without any element can be created as an instance of EmptyCoroutineContext.
We can merge two CoroutineContexts via the plus (+) operator. The notable design here is that an instance of Element is a singleton CoroutineContext by itself. Hence, we can easily create a new context by adding an element to a context:
val context = EmptyCoroutineContext
val newContext = context + CoroutineName("baeldung")
Assertions.assertTrue(newContext != context)
Assertions.assertEquals("baeldung", newContext[CoroutineName]!!.name)
Or we can remove an element from a CoroutineContext by calling CoroutineContext#minusKey:
val context = CoroutineName("baeldung")
val newContext = context.minusKey(CoroutineName)
Assertions.assertNull(newContext[CoroutineName])
3.2. Coroutine Context Elements
Kotlin has a bunch of implementations for CoroutineContext.Element to persist and manage different aspects of a coroutine:
- Debugging: CoroutineName, CoroutineId
- Life-cycle management: Job, which stores task hierarchy and can be used to manage the life-cycle
- Exception Handling: CoroutineExceptionHandler handles encountered exceptions within coroutine builders like launch that don’t propagate exceptions
- Thread management: ContinuationInterceptor, which listens to the continuation within a coroutine and intercepts its resumption. CoroutineDispatcher implementations are the most used types in this category. Moreover, the default element of ContinuationInterceptor is Dispatchers.Default
4. Dispatchers
CoroutineDispatcher is a subtype of the ContinuationInterceptor element of context. Therefore, it is responsible for determining the execution thread (or threads) of the coroutine.
When Kotlin executes a coroutine, it first checks if CoroutineDispatcher#isDispatchNeeded returns true or not. If yes, then CoroutineDispatcher#dispatch assigns the execution thread; otherwise, Kotlin executes the coroutine unconfined.
Kotlin has several implementations of CoroutineDispatcher, and there are some internal singleton instances: DefaultScheduler, CommonPool, DefaultExecutor, and Unconfined.
To pass the predefined scheduler, we can use kotlinx.coroutines.Dispatchers values:
- Dispatchers.Default: if we don’t set the system property kotlinx.coroutines.scheduler or enable it, it points to the DefaultScheduler singleton. Otherwise, it points to the CommonPool singleton.
- Dispatchers.Main: loads the main dispatcher and is only available if the required dependency exists in the classpath
- Dispatchers.Unconfined: a pointer to the Unconfined singleton
- Dispatchers.IO: a pointer to DefaultScheduler.IO
Let’s pass a dispatcher to a coroutine builder function:
launch(Dispatchers.Default) {
Assertions.assertTrue(
coroutineContext[ContinuationInterceptor]!!
.javaClass
.name.contains("DefaultScheduler")
)
}
Moreover, ThreadPoolDispatcher.kt has two obsolete public functions: newSingleThreadContext for a single thread execution and newFixedThreadPoolContext to assign a thread pool. As a replacement, we can create an instance of ExecutorService and pass it as the CoroutineDispatcher:
launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
Assertions.assertTrue(
coroutineContext[ContinuationInterceptor]!!
.javaClass
.name.contains("ExecutorCoroutineDispatcher")
)
}
4.1. Confined vs. Unconfined Dispatchers
By default, a dispatcher is inherited from outer CoroutineScope unless we explicitly pass a dispatcher to builder functions:
runBlocking(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
launch {
Assertions.assertTrue(
coroutineContext[ContinuationInterceptor]!!
.javaClass
.name.contains("ExecutorCoroutineDispatcher")
)
Assertions.assertTrue(Thread.currentThread().name.startsWith("pool"))
}
}
On the other hand, Dispatchers.Unconfined references the internal object Unconfined, which overrides the CoroutineDispatcher#isDispatchNeeded with false:
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
It causes the coroutine to start in the caller thread until the coroutine calls a suspend block, then resumes the suspending function’s thread:
runBlocking {
launch(Dispatchers.Unconfined) {
Assertions.assertTrue(Thread.currentThread().name.startsWith("main"))
delay(10)
Assertions.assertTrue(!Thread.currentThread().name.startsWith("main"))
}
}
Coroutines that are not CPU intensive and don’t update any shared data are appropriate for Dispatchers#Unconfined.
5. Coroutine Scope
As we understand from a prior discussion, CoroutineScope is an interface with only one property: coroutineContext. Furthermore, we can build coroutines using coroutine builder functions — extensions of CoroutineScope called async and launch. Both builder functions ask for three parameters:
- context (optional): if nothing passed, the default is EmptyCoroutineContext
- coroutineStart (optional): if nothing passed, it assumes CoroutineStart#DEFAULT. Other available options are LAZY, ATOMIC, and UNDISPATCHED
- suspend block: the executable code block within the coroutine
Our interest is in the context argument. To create a context for the new coroutine, the builder function adds the context argument to the current CoroutineScope#coroutineContext, then adds some configuration elements.
Next, the builder creates a coroutine instance from one of the implementations of AbstractCoroutine:
- for launch: StandaloneCoroutine, or LazyStandaloneCoroutine
- for async: DeferredCoroutine, or LazyDeferredCoroutine
Then, the builder passes the new context in the constructor.
The context of AbstractCoroutine is the parentContext (the context of the previous step) plus the coroutine itself. As AbstractCoroutine is both a CoroutineScope and a Job, so the coroutine context contains a Job element:
public final override val context: CoroutineContext = parentContext + this
**GlobalScope is a singleton CoroutineScope, but without any bounded job and with an EmptyCoroutineContext. Although we have to avoid using it with coroutine builders, top-level coroutines or unconfined ones can use it.
**
6. Conclusion
In this article, we’ve learned about CoroutineContext and Dispatchers.
As always, the complete code of samples in the article is available over on GitHub.