You read 20 articles, watched 6 videos, asked your colleagues 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 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 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 Coroutines. 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; 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, which 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
class CarFragment : Fragment {
init {
lifecycleScope.launch {
// You can run a suspend function here, it'll be canceled when the Lifecycle is destroyed
}
}
}
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 takes 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 thread 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 optimizes those calls, staying on the same dispatcher by following a fast-path.
// Extracted from Builders.common.kt
public suspend fun <T> withContext(...) : T {
...
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
...
}
}
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 the following articles:
- Kotlin Coroutines: The Suspend Function
- Coroutines on Android (part I): Getting the background
- CoroutineDispatcher
- 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