
Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 19, 2024
In this tutorial, we’ll learn about the CoroutineContext and then continue with dispatchers as one of the important elements of the CoroutineContext.
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.
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])
}
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])
Kotlin has a bunch of implementations for CoroutineContext.Element to persist and manage different aspects of a coroutine:
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:
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")
)
}
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.
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:
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:
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.
In this article, we’ve learned about CoroutineContext and Dispatchers.