Series - Kotlin coroutines
- Launching your coroutine knowledge
- Waiting for coroutines
- Cancelling coroutines
- Cancelling child coroutines
- Async/await in coroutines
Coroutines are essentially lightweight threads that allow your applications to squeeze more juice out of the machines they are running on, without needing to manually manage the threads executing them. Coroutines add a new abstraction, further removing you (the developer) away from the lifecycle of threads. This abstraction allows a single thread to context switch between various tasks and therefore making better use of the thread’s time. It spends less time sitting around awaiting a result (from a potentially slow I/O task) and can instead continue to be useful by working on a task issued by another coroutine.
Let’s try thinking of a real-life example. Think of your day as a developer. You build your code, and it takes, let’s say, 3 mins to do so. During that time, what do you do? Well, yes, of course, some times you might just sit there and literally do nothing. But most of the time, you’ll check your emails, Slack, Twitter or go for a stretch. It doesn’t really matter what you’re doing, it’s the fact that you are doing something else. You didn’t just sit there and wait for the build to finish. You saturated your own processing and did something else while you waited. That is the concept that coroutines provide.
Yes, I did just compare you to a thread. You spawn, you live, and you die. Some of you are even killed… Ok, that metaphor got a bit dark.
One last comment before we dive into a small example. The concept of lightweight threads that coroutines introduce to Kotlin is not new. Many other languages include this ability, for example.
- Erlang
- Go has goroutines
- Java is working on Project Loom
Quick setup
Coroutines are not included in Kotlin’s standard library (although some keywords are baked in). You will need to add a dependency on kotlinx-coroutines-core
to access coroutines.
-
Gradle:
dependencies { compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.5' }
-
Maven:
<dependencies> <dependency> <groupId>org.jetbrains.kotlinx</groupId> <artifactId>kotlinx-coroutines-core</artifactId> <version>1.3.5</version> </dependency> </dependencies>
The version will change as the library is updated (yes, I have to say this…)
A simple coroutine
As this is an introduction to coroutines, we will only go over an example with limited scope. I want to point out that a lot of the benefits of coroutines will not be highlighted by these examples. Moving forward, I will cover these topics which will show you more interesting use-cases.
Let’s jump right in:
fun main() {
runBlocking {
launch {
delay(2000)
println("and it seems to work!")
}
print("This is your first coroutine, ")
}
}
In this example, we have some code that launches a coroutine with a delay
before continuing to print out a message. Even in this short example, there are a few things we can extract from it. Let’s start from the top and work our way down.
runBlocking
- Runs a new coroutine that blocks the current thread until everything inside of it (including other coroutines) has completed. This is only here to make the example work. Realistically, you should only use this to move from blocking to non-blocking code, such as inmain
(like the example) or in tests.launch
- Launches a new coroutine. UnlikerunBlocking
,launch
does not block the current thread. Furthermore, it inherits theCoroutineContext
from the currentCoroutineScope
, these magic words will be expanded on later.delay
- Suspends the coroutine it is executed in for a given amount of time. In one way it is similar toThread.sleep
in that it stops the coroutine from continuing for a while. But, it does a lot more than that. The thread that the coroutine is running on does not get blocked (like aThread.sleep
would). Instead, the coroutine suspends. Releasing its thread to allow another coroutine to continue executing. Once thedelay
period is up, and a thread is available, the coroutine will continue where it left off.
All functions shown above are part of
kotlinx-coroutines-core
Quite a few words and concepts were thrown just thrown at you. I would be a monster to not go through them.
- Suspending a coroutine - Conceptually, a coroutine suspending is like the passing of a baton in a never-ending relay race, allowing the next runner to progress. As you move into a multi-threaded environment though, this example becomes a bit weird as everyone is passing the baton to semi-random runners, but let’s ignore that. Coroutines share threads (for example, from a thread pool). Suspending a coroutine frees up the thread it was executing on to allow another to take over. This is the main benefit of coroutines. Allowing the number of running coroutines to significantly surpass the number of threads available to the application.
CoroutineContext
- The persistent context of a coroutine, that decides what threads the asynchronous code found inside of it executes on.CoroutineScope
- Consists of a singleCoroutineContext
property that is provided to any new (child) coroutines that are launched inside it. Most functions that create new coroutines are extension functions ofCoroutineScope
, therefore requiring a scope to before anything useful can be done. In the example above,runBlocking
is providing itsCoroutineScope
to the following call tolaunch
.
This blog post by Roman Elizarov (Team Lead for Kotlin libraries) goes into the differences between
CoroutineContext
andCoroutineScope
Suspending functions
The topic of suspending a coroutine was touched on above, in this section, we will cover the suspend
keyword.
You have already seen one suspending function from the example above, delay
. Below are the contents of delay
:
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
The function is short enough that I’ve included all of its contents, but don’t worry about understanding it all. We are focusing on the suspend
keyword. Notice that I said keyword. suspend
is built into the Kotlin language itself.
So, why do we need it in the first place?
All functions that suspend at some point during their execution must be annotated with suspend
. I mean duh, right? Trying to go deeper than this to fully understand when a function suspends can quickly become confusing. Quite frankly, I haven’t use coroutines enough yet to give you a more in-depth explanation.
So for now, remember this is a getting started post of sorts and follow this rule of thumb. If you call a function prefaced with suspend
, and the calling point is not inside a coroutine, then your function should also be marked with suspend
.
For example:
-
Calling point outside a coroutine:
suspend fun printAfterDelay() { delay(2000) println("and it seems to work!") }
-
Calling point inside a coroutine:
fun CoroutineScope.printAfterDelay() { launch { delay(2000) println("and it seems to work!") } }
Both of these are a slight refactoring of the original example.
The compiler will help you in a lot of situations (a benefit of being a keyword). For example, trying to compile:
fun printAfterDelay() {
delay(2000)
println("and it seems to work!")
}
Note that the suspend is missing
Will lead to the following error:
Suspend function 'delay' should be called only from a coroutine or another suspend function
Which pretty much reiterates what I said previously.
I believe that should be enough of an explanation to get you started.
Summary
Kotlin’s coroutines are lightweight threads that share and release their underlying resources whenever a single coroutine reaches a suspension point. This allows better utilisation of an application’s resources, as threads are not necessarily blocked when performing long-running tasks. We looked at a rudimentary example consisting of a single coroutine and a suspending function. Finally, we took a modest look at the suspend
keyword and how you add it to functions that suspend.
The real benefits of coroutines have not been touched on through the examples in this post. Instead, it has focused on laying the groundwork as you begin building your coroutine knowledge. Moving forward, I aim to publish more content on Kotlin coroutines. So look out for those, assuming I keep my word…
If you enjoyed this post or found it helpful (or both) then please feel free to follow me on Twitter at @LankyDanDev and remember to share with anyone else who might find this useful!