1. Introduction
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.
2. Kotlin Coroutine
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.
3. What Is async-await?
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)
4. What Is withContext?
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.
5. async-await vs. withContext
Let’s sum up our findings about these two features.
- When we need to collect the result of a coroutine, then we use withContext or async
- withContext has the same functionality as async followed by await but with less overhead
- When we need parallel code execution, then we put them in several async blocks and finally await for all of them
- With async, we have to catch the exceptions of the code block inside the async body. Otherwise, it can terminate the parent scope
6. Conclusion
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.
As always, the complete code of this article is available over on GitHub.