You read 20 articles, watched 6 videos, asked your colleges about it and somehow you still can’t understand how Coroutines work, you’re probably thinking it’s impossible to learn Coroutines. I felt the same before I started writing this article.
I spent the last few days learning as much as I could about Coroutines and I’ll distill all that in this 3 part series. It’ll answer questions such as “What’s the difference between a CoroutineScope and a CoroutineContext”, “Do I need to switch the dispatcher?”, “What is Structured Concurrency?”, “How to handle cancellation?”, etc…
I won’t go into detail on how Coroutines work, but rather on how to use them well. This article is mainly focused on Coroutines for Android but most concepts apply to Kotlin in general.
What are Coroutines?
Kotlin coroutines introduced a new style of concurrency that can be used to simplify asynchronous code. If you have you used callbacks in the past, you may know how messy things can get, there’s even a name for that, Callback Hell.
Besides making asynchronous code easier to write, kotlinx.coroutines also has some modules to help you with other things such as writing reactive code.
Why use Coroutines?
If you’re using Kotlin on the backend, you’re probably using Ktor and it works asynchronously by using Couroutines. Most of its functions are suspended so you need to write suspended code to take advantage of that.
If you’re using Android, you know every app has a main thread that is in charge of handling UI and coordinating user interactions. If there is too much work happening on this thread, the app appears to hang or slow down, leading to an undesirable user experience. By using Coroutines and writing main-safe functions these problems can be easily avoided.
Why not simply use RxJava?
Most people have been using RxJava for years and migrating an app to Coroutines is not an easy task. The real power of RxJava is reactive programming and back pressure. If you’re using it to control async requests, you’re basically using a bazooka to kill a spider. It will do the job, but it’s complete overkill. Besides that, suspended code doesn’t rely on callbacks making the code more linear and easier to understand.
Suspend
The suspend
modifier is the central piece of Coroutines. A suspending function is simply a function that can be paused and resumed at a later time.
suspend fun yourFunction() {
// code
}
Suspending functions allow you to pause the execution of the current coroutine without blocking the thread. This implies that the code you’re looking at may pause execution when it calls a suspending function and restart execution later.
The biggest benefit of suspending functions is that we can reason sequentially about them.
Why should you even care about that if operating systems nowadays support multiple threads?
- UI applications often have a single main thread that handles all UI interactions and events. When this thread is blocked, the entire program becomes unresponsive.
- Backend programs generally handle a large number of concurrent requests, which are typically scheduled to run in a thread pool of a certain size. When requests are processed fast, everything is fine, but when you have a slow service, it might end up blocking all threads.
Main-safety with Coroutines
One common misunderstanding is that adding a suspend modifier to a function makes it either asynchronous or non-blocking. Suspend does not instruct Kotlin to execute a function in a background thread. Suspending functions are only asynchronous if used expressly as such.
Coroutines can run on the main thread, for example when launching a coroutine in response to a UI event and that’s fine given they don’t block.
The rule of thumb is that suspending functions never block the caller thread, making them main-safe functions.
CoroutineContext
The CoroutineContext
is a collection of elements that define a coroutine’s behavior, that means you can change the behavior of a coroutine by adding elements to the CoroutineContext
. It consists of:
- Job — controls the lifecycle of the coroutine.
- CoroutineDispatcher — defines the thread the work will be dispatched to.
- CoroutineExceptionHandler — handles uncaught exceptions.
- CoroutineName — Adds a name to the coroutine (useful for debugging).
CoroutineContexts can be combined using the + operator, the result is a new CoroutineContext
. The already existing elements are overwritten.
Using the same type of element when combing contexts throws an exception at runtime.
Dispatchers.Main + Dispatchers.IO
// Using 'plus(CoroutineDispatcher): CoroutineDispatcher' is an error. Operator '+' on two CoroutineDispatcher objects is meaningless.
CoroutineScope
A CoroutineScope
keeps track of all coroutines it creates. Therefore, if you cancel a scope, you cancel all coroutines it created. The ongoing work (running coroutines) can be canceled by calling scope.cancel()
at any point in time.
That doesn’t mean the coroutines will stop running as soon as you call cancel, a coroutine code has to cooperate to be cancellable, we’ll take a look at how to do that later.
You should create a CoroutineScope
whenever you want to start and control the lifecycle of coroutines in a particular layer of your app.
val scope = CoroutineScope(Dispatchers.Default)
In Android, there are KTX libraries that already provide a CoroutineScope in certain lifecycle classes such as viewModelScope
and lifecycleScope
.
If your ViewModel gets destroyed, all the asynchronous work that is going on is stopped. That way you don’t waste resources.
If you consider that certain asynchronous work should persist after ViewModel destruction, it is because it should be done in a lower layer of your app’s architecture.
Manuel Vivo
For Lifecycle objects it’s pretty similar
CoroutineDispatcher
The CoroutineDispatcher
is in charge of dispatching the execution of a coroutine to a thread.
The following implementations are provided by Kotlin:
- Dispatchers.Default — All conventional builders utilize it. It makes advantage of a pool of shared background threads. This is a good option for compute-intensive coroutines that need CPU resources.
- Dispatchers.IO — It is meant to offload IO-intensive blocking tasks (such as file I/O and blocking socket I/O) by using a common pool of on-demand generated threads.
- Dispatchers.Unconfined — It starts the coroutine execution in the current call-frame and continues until the first suspension, at which point the coroutine constructor method returns. The coroutine will subsequently restart in whichever thread was utilized by the associated suspending function, without being bound to any particular thread or pool. In most cases, the Unconfined dispatcher should not be utilized in code.
You can also execute coroutines in any of your thread pools by converting them to a CoroutineDispatcher
using the Executor.asCoroutineDispatcher()
extension function. Private thread pools can be created with newSingleThreadContext and newFixedThreadPoolContext.
Some libraries have their own dispatchers so you don’t need to worry about switching the dispatcher to use them.
- Room provides main-safety automatically if you use suspend functions, RxJava, or LiveData.
- Retrofit and Volley manage their own threads and do not require explicit main-safety in your code when used with Kotlin coroutines.
Switching threads
withContext
is used to call a suspending block with a given coroutine context. You’ll be using it most of the time to switch the dispatcher the coroutine will be executed on.
withContext(Dispatchers.IO) {
yourFunction()
}
Because withContext
lets you control what thread any line of code executes on without introducing a callback to return the result, you can apply it to very small functions like reading from your database or performing a network request (only do that if the library you’re using doesn’t). So a good practice is to use withContext
to make sure every function is safe to be called on any Dispatcher including Main — that way the caller never has to think about what thread will be needed to execute the function.
Performance of withContext
Whenever a threads stops executing for another one to execute a Context switch happens. This whole process is not cheap and should be avoided as much as possible. withContext
can be used to change the CoroutineDispatcher
and consequently the thread a coroutine is running, thereby causing a Context switch. Shouldn’t withContext
be avoided because of this overhead?
The CoroutineScheduler
, which is the thread pool utilized by default in the JVM implementation, distributes dispatched coroutines to worker threads in the most efficient way possible. Because Dispatchers.Default
and Dispatchers.IO
use the same thread pool, moving between them is streamlined to prevent thread changes wherever feasible.
The coroutines library even optimize those calls, staying on the same dispatcher by following a fast-path.
In this article we learned about the 4 building blocks of coroutines: suspend
, CoroutineCoxtext
, CoroutineScope
and CoroutineDispatcher
. Just knowing about them doesn’t allow you to do much, that’s why we’re going to learn how to use them in real code in part II by learning about Job
, SupervisorJob
, launch
and async
.
Coroutines is not a simple topic, so don’t worry if you didn’t understand much of what you’ve read, go back to the beginning and read it again. I also recommend you read following articles:
Kotlin Coroutines: The Suspend Function
Coroutines on Android (part I): Getting the background
Easy Coroutines in Android: viewModelScope
Guide to UI programming with coroutines
If you have any doubts, feel free to contact me. See you in the next article 😉
Cover Photo by Kelly Sikkema on Unsplash