3.
Getting Started with Coroutines
Written by Filip Babić
So you’ve heard a lot about working with asynchronous or concurrent programming. It’s time for you to learn a bit more about coroutines and how they work in the background (pun intended).
In this chapter, you will:
- Learn about routines and how a program controls its execution flow.
- Learn about suspendable functions and suspension points in code.
- Launch your first Kotlin coroutine, creating jobs in the background.
- Practice what you’ve learned by creating a few typical tasks, including posting to the UI thread.
Let’s get started with routines!
Executing routines
Every time you start a process — launching an application, for instance — your computer creates something called a main routine. This is the core part of every program because it’s where you set up and run all the other components in your code. As in the most basic learning samples, you often have a main function, which prints Hello World
. That main function is the entry point of your program and is part of the main routine.
But as your programs gets bigger, so does the number of functions and the number of calls to other functions. Whenever you call some other function in the main block, you start something called a subroutine. A subroutine is just a routine, nested within another routine. The computer places all of these routines on the call stack, a construct that keeps track of what’s currently running and how the current routine has been called. When a subroutine is finished running it is popped off the stack, and control is passed back to the caller routine. Lastly, if the stack is empty, and there’s nothing else to run, the program finishes.
Invoking a subroutine is like doing a blocking call. A coroutine is then a subroutine that you can invoke as a non-blocking call. Because of this, the main difference between a standard subroutine and a coroutine is that the latter can run in parallel with other code. You can start and forget them, moving on to the rest of the program.
Launching a coroutine
To follow the code in this chapter, import this chapter’s starter project, using IntelliJ, and selecting Import Project, and navigating to the getting-started-with-coroutines/projects/starter folder, selecting the getting_started_with_coroutines project.
When the project opens, locate and open Main.kt. There, you will find the following code:
fun main() {
(1..10000).forEach {
GlobalScope.launch {
val threadName = Thread.currentThread().name
println("$it printed on thread ${threadName}")
}
}
Thread.sleep(1000)
}
Since launching your first coroutine is not that fascinating, you’ll launch your first ten thousand coroutines! Now, launching ten thousand threads is a bit tedious for a computer, and most programs would get an OutOfMemoryException
. But since coroutines are extremely lightweight, you’re able to launch a large number of them, without any performance impact. If you run the program, you should see a lot of text, each line saying which number it is printing and on which thread.
There are a few important things to notice in the snippet above. The first is about the coroutine body that is represented by the block of code passed as the parameter to launch()
, which is called a coroutine builder.
Second, when launching coroutines, you have to provide a CoroutineScope
because they are background mechanisms which don’t really care about the lifecycle of their starting point. What would happen if the program ended before the completion of the coroutine body? In this case, you use something called the GlobalScope
, which makes explicit the fact that the coroutine lifecycle is bound to the lifecycle of the application. Because of this, you also need to put the current thread on hold, calling Thread.sleep(1000)
in the end of main()
.
This is the basic explanation of what you’re doing, but these concepts are more complex than that.
Building coroutines
You’ve heard the term launching coroutines quite a few times now. In truth, you first have to use a coroutine builder. The Coroutines library has several coroutine builder functions for you to use to start a new coroutine. In the previous example, you used launch()
with the following signature:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
As you can see, launch()
has a few arguments that you can pass in: a CoroutineContext
, a CoroutineStart
and a lambda function, which defines what’s going to happen when you launch the coroutine. The first two are optional.
A CoroutineContext
is a persistent dataset of contextual information about the current coroutine. This can contain objects like the Job
and Dispatcher
of the coroutine, both of which you will touch on later. Since you haven’t specified anything in the snippet above, it will use the EmptyCoroutineContext
, which points to whatever context the specified CoroutineScope
uses. You can create custom contexts if you’d like, but for the most part, the existing ones are sufficient.
The CoroutineStart
is the mode in which you can start a coroutine. Options are:
- DEFAULT: Immediately schedules a coroutine for execution according to its context.
- LAZY: Starts coroutine lazily.
-
ATOMIC: Same as
DEFAULT
but cannot be cancelled before it starts. - UNDISPATCHED: Runs the coroutine until its first suspension point.
Last but not least, you specify a lambda block with the code that the coroutine will execute. If you check the previous definition of launch()
, you will notice that this lambda block has a somewhat different signature than standard lambda blocks. Its signature is block: suspend CoroutineScope.() -> Unit
. It’s a lambda with a receiver of type CoroutineScope
. This allows you to have nested jobs, as you can launch more coroutines from another launch
block. Another thing that is specific is the suspend
modifier.
As you’ve learned, coroutines build upon the concept of suspendable functions. You can use the modifier at hand to mark a lambda or another function suspendable. You’ll learn a bit more about suspendable functions in the next chapter.
Scoping coroutines
As you’ve learned, coroutines can be launched in parallel with the main execution of a program. However, this doesn’t mean that if the main program finishes, or stops, the coroutines will do the same. Or at least it didn’t in the first few versions of the API. This behavior leads to subtle bugs in which applications would execute tasks even if you closed the application.
To mitigate these cases, the Coroutines API team created the CoroutineScope
. Each scope instance knows which context it’s related to, and each scope has its own lifecycle. If the lifecycle for your selected scope ends, while it’s trying to run coroutines, all the work, even if in progress, will stop. This is why, if you try running the snippet without Thread.sleep
, there may not be any output or there may be only some.
Since you have to call launch()
on a CoroutineScope
, there are two ways of doing this. You can use the GlobalScope
, as you did so far, not caring about where exactly the coroutine is launched. Or you can implement the CoroutineScope
interface, and provide an instance of the CoroutineContext
in which you’ll run coroutines. The former is easier, and it’s a great option when you don’t care about coroutine results, posting to the UI thread or about the job completion. The latter is crucial if you want to specify where you need to use the result (like the UI thread), and when you want to bind the jobs to the lifecycle of a certain object instance, like Activity
instances in Android.
There are cases in which the lifecycle or manual cancellation don’t necessarily cancel the coroutines. It’s not only important that you provide cancellation mechanisms, but that you also have to write cooperative code. This means that your functions check whether or not their wrapping Job
is running. You’ll see how to do this later in the chapter.
You should have a better understanding of how coroutines work and what’s important to define when launching them. In the next few sections, you’ll learn a bit more about different functionalities coroutines have and, finally, you’ll see how to combine jobs running a few different tasks using launch()
.
Explaining jobs
If you’ve noticed, most things in coroutines refer to a job, which you create and run. A Job
is also what launch()
returns. But what is a Job
and what can you do with it?
When you launch
a coroutine, you basically ask the system to execute the code you pass in, using a lambda expression. That code is not executed immediately, but it is, instead, inserted into a queue.
A Job
is basically a handle to the coroutine in the queue. It only has a few fields and functions, but it provides a lot of extensibility. For instance, it’s possible to introduce a dependency relation between different Job
instances using join()
. If Job
A invokes join()
on Job
B, it means that the former won’t be executed until the latter has come to completion. It is also possible to set up a parent-child relation between Job
instances using specific coroutine builders. A Job
cannot complete if all its children haven’t completed. A Job
must complete in order for its parent to complete.
The Job
abstraction makes this possible through the definition of states, whose transitions follow the workflow described by the diagram on the next page:
When you launch a coroutine, you create a Job
, which is always in the New state. It then goes directly into the Active state by default, unless you’ve supplied the LAZY
CoroutineStart
parameter in the coroutine builder you’ve used. You can also move a Job
from the New to the Active state using start()
or join()
. A running coroutine is always in the Active state. As you can see in the state diagram, the Job
can complete or can be canceled.
It’s very important to note how completion and cancellation work for dependent Job
instances. In particular, you can see that a Job
remains in the Completing state until all of its children complete. It’s important to say that the Completing state is internal and, if queried from outside, the Job
will result in the Active state.
States are fundamental because they give you information about what’s going on with the coroutines and what you can do with them. You can also query the state of a Job
or simply iterate over the children and do something with them.
Creating a Job
is pretty easy and nesting isn’t hard either. You’ve seen how they work with completion, but how do things work in the case of cancellation or errors?
Canceling Jobs
When you launch a coroutine and you create a Job
, many things can happen. An exception can occur, or you might need to cancel the Job
because of some new conditions in the application. Consider, for instance, a list of images that you download from the network. Every time you need to display an image in a list item, you start a coroutine for the download. This download might fail because there’s no connection, and you have to handle the related exception. Or the download might be canceled because the user scrolls the list and the image goes out of the screen before it’s available. It’s very important to understand how you can manage these use cases when using coroutines.
Usually, an uncaught exception would cause the entire program to crash. However, since coroutines have suspending behavior, if an exception occurs, it can also be suspended and managed later.
Much easier is the way you can handle the cancellation. You can do it by invoking cancel()
on the related Job
. The system is then smart enough to understand the dependencies between Job
s. If you cancel a Job
, you automatically cancel all its children. If it has a parent, the parent is canceled. A parent of a Job
is also canceled if one of its children fails.
Note: It’s possible to use a special parent job, which doesn’t require for all the children to complete happily - they can be cancelled or can fail independently. This version of a
Job
is called theSupervisorJob
.
As mentioned before, even though you cancel a Job
, your code might not be co-operative with the cancellation events. You can check this by using its isActive
property. If your code does computational work, without checking the isActive
flag, it won’t listen to cancellation events. So running while
loops with the isActive
flag is safer than with your own conditions. Or you should at least try to depend on isActive
, on top of your conditions.
Digging deeper into coroutines
So far you’ve launched a large amount of coroutines, and you’ve seen how you can create multiple coroutine jobs. But there are other things you can do when launching a coroutine. For example, if you have some work that you have to first delay for a period of time, before running, you can do so with delay()
. Open up Main.kt again, and replace the code with the following snippet:
fun main() {
GlobalScope.launch {
println("Hello coroutine!")
delay(500)
println("Right back at ya!")
}
Thread.sleep(1000)
}
If you run the code above, you should see “Hello coroutine,” in the console, and, briefly after that, “Right back at ya.” delay()
is really useful because you can effectively wait for the given amount of time and then run work when everything is ready. And most importantly it does not sleep a thread, or block, it just suspends the coroutine.
Dependent Job
s in action
So far, you’ve learned that, every time you launch a coroutine, you can get a Job
reference. You can also create dependencies between different Job
s — but how? Just replace the previous code with this:
fun main() {
val job1 = GlobalScope.launch(start = CoroutineStart.LAZY) {
delay(200)
println("Pong")
delay(200)
}
GlobalScope.launch {
delay(200)
println("Ping")
job1.join()
println("Ping")
delay(200)
}
Thread.sleep(1000)
}
Going through the code above:
- You first launch a coroutine that contains some delays and prints the
Pong
word, saving the createdJob
into thejob1
reference. - Then, you launch a second coroutine that contains a couple of
println
s but also invokes thejoin
function onjob1
.
What is the expected output? If you follow the code, you would expect to see Pong
and then Ping
twice, but this is not the case. As you can see, you used the CoroutineStart.LAZY
value as CoroutineStart
, and this means that the related code is going to be executed only when you actually need it.
This happens when the second coroutine invokes the join
function on job1
. This is why the result of the previous code is Ping
, then Pong
and, finally, Ping
again.
Managing Job hierarchy
In the previous code, you created a dependency between different Job
instances, but this is not the kind of relation you can refer to as a parent-child relation. Again, replace the previous code with the following. You can use with()
in order to avoid the repetition of the GlobalScope
receiver:
fun main() {
with(GlobalScope) {
val parentJob = launch {
delay(200)
println("I’m the parent")
delay(200)
}
launch(context = parentJob) {
delay(200)
println("I’m a child")
delay(200)
}
if (parentJob.children.iterator().hasNext()) {
println("The Job has children ${parentJob.children}")
} else {
println("The Job has NO children")
}
Thread.sleep(1000)
}
}
Going through the above code, in turn:
- Here, you launch a coroutine and assign its
Job
to theparentJob
reference. - Then, you launch another coroutine using the previous
Job
as theCoroutineContext
. This is possible because theJob
abstraction implementsCoroutineContext
. Under the hood, theCoroutineContext
you pass here is merged with the one from the currently activeCoroutineScope
-EmptyCoroutineContext
.
If you run the code above, you can see how the parentJob
has children
. If you run the same code, removing the context for the second coroutine builder, you can see that the parent-child relationship is not established and the children are not present.
Using standard functions with coroutines
Another thing you can do with coroutines is build retry-logic mechanisms. Using repeat()
from the standard library, paired up with delay()
you learned above, you can create code that attempts to run work in delayed periods of time. Once again, replace the Main.kt code with the next snippet:
fun main() {
var isDoorOpen = false
println("Unlocking the door... please wait.\n")
GlobalScope.launch {
delay(3000)
isDoorOpen = true
}
GlobalScope.launch {
repeat(4) {
println("Trying to open the door...\n")
delay(800)
if (isDoorOpen) {
println("Opened the door!\n")
} else {
println("The door is still locked\n")
}
}
}
Thread.sleep(5000)
}
Try running the code. You should see that someone’s trying to open the door a few times before ultimately succeeding. So using delay()
, and repeat()
from Kotlin’s standard library, you managed to build a mechanism that tries to run some code multiple times, before you meet a time or logic condition. You can use the same flow to build networking back-off and retry logic. And once you learn how to return values from coroutines later in this book, you’ll see how powerful this can be.
Posting to the UI thread
From what you’ve seen so far, coroutines are all about simplicity, with a large part of their functionality built into the language itself. Posting to the UI thread isn’t complicated; it comes down to starting a new coroutine with a UI dispatcher as its threading context.
Since we’re talking about applications with a visible user interface, you can post to the main thread in Android, Swing and JavaFx applications. You can do it using Dispatchers.Main
as the context in the following way:
GlobalScope.launch(Dispatchers.Main) { ... }
You need to be careful, though, because this is not enough. You need to set one of the following dependencies:
implementation ’org.jetbrains.kotlinx:kotlinx-coroutines-android:...’
implementation ’org.jetbrains.kotlinx:kotlinx-coroutines-swing:...’
implementation ’org.jetbrains.kotlinx:kotlinx-coroutines-javafx:...’
Otherwise, you’ll get an exception like this:
Exception in thread "DefaultDispatcher-worker-3" java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. ’kotlinx-coroutines-android’
You can try this behavior with a simple Swing example. First, you need to add this dependency to the build.gradle
:
"implementation ’org.jetbrains.kotlinx:kotlinx-coroutines-swing:$kotlin_coroutines_version"
Then, you can replace main()
with this:
fun main() {
GlobalScope.launch {
val bgThreadName = Thread.currentThread().name
println("I’m Job 1 in thread $bgThreadName")
delay(200)
GlobalScope.launch(Dispatchers.Main) {
val uiThreadName = Thread.currentThread().name
println("I’m Job 2 in thread $uiThreadName")
}
}
Thread.sleep(1000)
}
The external coroutine prints the name of the thread it’s executed in. After a short delay, you launch another coroutine using Dispatchers.Main
as CoroutineContext
. This is the one that allows you to interact with the main thread.
If you run the code, you’ll get something like:
I’m Job 1 in thread DefaultDispatcher-worker-1
I’m Job 2 in thread AWT-EventQueue-0
The first Job
has been executed in the background by a worker thread. The second is the main thread in Swing. Pretty simple, right?
To check out the examples from this chapter, import this chapter’s final project, using IntelliJ, and selecting Import Project, and navigating to the getting-started-with-coroutines/projects/final folder, selecting the getting_started_with_coroutines project.
Key points
- You can build coroutines using coroutine builders.
- The main coroutine builder is the launch function.
- Whenever you launch a coroutine, you get a
Job
object back. -
Jobs can be canceled or combined together using the
join
function. - You can nest jobs and cancel them all at once.
- Try to make your code cooperative — check for the state of the job when doing computational work.
- Coroutines need a scope they’ll run in.
- Posting to the UI thread in advanced applications is as easy as passing in the
Dispatchers.Main
instance as the context. - Coroutines can be postponed, using the
delay
function.
Where to go from here?
You’re ready to launch as many coroutine jobs as you want! But this is only a small piece of the Kotlin coroutine API. So far, you’ve only launched Job
s, pieces of work that you need to finish. The real power of suspending code is being able to return values asynchronously, without any callbacks or additional mechanisms.
In the next chapter, you’ll learn a bit more about the fundamentals of coroutines and how code is suspended in programs. You’ll learn about the execution of programs, how the computer passes directions to functions and how the program knows where to go back once a suspended function returns.
So let’s not leave you in suspense!