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, our goal is to gain insight into async and withContext in the coroutines world. We have a short journey to see how to use these two methods, what their similarities and differences are, and where to use each method.
Coroutines are strong tools for writing asynchronous code with a fluent API in a sequential style without the headache of reactive style coding. Kotlin introduced coroutines as part of the language. Moreover, kotlinx-coroutines-core is a library for more advanced usage of coroutines.
A coroutine is executed within a coroutine context, which consists of several CoroutineContext.Elements. Base elements are CoroutineId, CoroutineName, CoroutineDispatcher, and Job.
The CoroutineDispatcher assigns a coroutine to an executor. Dispatchers can use different strategies for dispatch based on different executors like event loop and thread pool, or they can even leave a coroutine unconfined.
CoroutineScope allows us to manage a coroutine by its associated Job instance. A coroutine can access the Job with coroutineContext[Job]. Job is an interface to manage coroutine lifecycle and reflect its states like active, completed, or canceled.
Now, let’s get a closer look at async and withContext and their usage.
async is an extension for CoroutineScope to create a new cancelable coroutine. Hence, it returns a Deferred object that holds the future result of the code block. We can cancel the coroutine by calling Deferred#cancel.
The async function follows structured concurrency. Therefore, it will cancel the outer coroutine in case of failure:
Assertions.assertThrows(Exception::class.java) {
runBlocking {
kotlin.runCatching {
async(Dispatchers.Default) {
doTheTask(DELAY)
throw Exception("Exception")
}.await()
}
}
}
By default, the async coroutine starts execution just as it’s created. However, we can change this behavior by passing a CoroutineStart arg:
async(Dispatchers.Default, CoroutineStart.LAZY)
Moreover, if we have multiple tasks independent from each other, we can start them with async to be executed concurrently. If we need the joined result, we can wait for all coroutines to complete by calling Deferred#await on each item:
val time = measureTimeMillis {
val task1 = async { doTheTask(DELAY) }
val task2 = async { doTheTask(DELAY) }
task1.await()
task2.await()
}
Assertions.assertTrue(time < DELAY * 2)
withContext is a scope function that allows us to create a new cancelable coroutine. If we pass a CoroutineContext arg, withContext merges the parent context and our arg to create a new CoroutineContext, then executes the coroutine within this merged context.
We also can pass a dispatcher to this function so that the execution of the block will happen on a thread from the passed dispatcher. When the execution is complete, the control returns back to the previous dispatcher.
If we have multiple blocks of withContext within a parent block, the execution of each of them suspends the parent thread, but they will each execute sequentially, one after another:
val time = measureTimeMillis {
val dispatcher = newFixedThreadPoolContext(2, "withc")
withContext(dispatcher) {
doTheTask(DELAY)
}
withContext(dispatcher) {
doTheTask(DELAY)
}
}
Assertions.assertTrue(time >= DELAY * 2)
Furthermore, withContext has an extension called coroutineScope that uses current context. Therefore, no context switch will happen.
Let’s sum up our findings about these two features.
In this article, we had a quick introduction to coroutines. Then we went through async-await and withContext usages, and finally, we have formulated where to use which one.