Kotlin Coroutines Introduction - mariamaged/Java-Android-Kotlin GitHub Wiki
Kotlin Coroutines - Introduction
Kotlin is an ever-evolving language, with a steady stream of new releases.
- 2018 saw the release of Kotlin 1.3, and perhaps the pre-eminent feature in that release was the coroutine system.
- Google already is supporting coroutines in some of the
Android Jetpack Kotlin extension libraries
and will expand upon those in the upcoming years.
The Problems
Doing Work Asynchronously
We often need to work
asynchronously
, and frequently that implies the use of threads.
For example:
- For the performance of a Web browser to be reasonable, we need to:
- Download assets (images, JavaScript files, etc.)
- In parallel across multiple threads.
- The bottleneck tends to be the
network
, so we:
- Parallelize a bunch of requests.
- Queue up the remainder.
- And get whatever other work done that we can while we wait for the network to give us our data.
Getting Results on a "Magic Thread"
- In many environments, one or more threads are special with respect to our background work:
- In
Android
,JavaFx
, and otherGUI environments
,updates to the UI
aresingle-threaded
, with aspecific thread
being responsible for those updates.- Typically, we have to do
substantial work
onbackground threads
, so we do not tie up the UI thread and prevent it from updating the UI. - So, while the long-running work might need to be done on a background thread, the UI effects of that work need to be done on the UI thread.
- Typically, we have to do
- In
Web app development
, traditional HTTP verbs (e.g., GET, PUT, POST) aresynchronous requests
.- The Web app
forks a thread
to respond to a request, and it is up to the code executing on that thread toconstruct
andreturn
theresponse
. - Even if that thread
delegates work
tosome other thread
(e.g., a processing thread pool). - Or
process
(e.g., a microservice). - The Web request thread needs to
block
waiting for theresults
(or for some sort oftimeout
), so that thread can built theappropriate response
.
- The Web app
- In
Callbacks
- Callbacks are simply
objects
representingchunks of code
to beinvoked
when acertain condition works
. - In the case of asynchronous operations, the "certain condition" often is:
- When the operation completes successfully.
- When the operation completes with an error.
- In some languages, callbacks could be implemented as
anonymous functions
orlamda expressions
, while in other languages they might need to beimplementations of certain interfaces
.
Callback hell occurs when you have a lot of nested callbacks, such as this Java snippets:
- Here,
doSomething()
,doTheNextThing()
, anddoSomethingElse()
might all arrange to do work onbackground threads
, callingmethods
on thecallbacks
when that work completes.
What Would be Slick
The ugliness comes from the challenges in following the execution flow through layers upon layers of callbacks.
- Ideally, the syntax for our code would not be inextricably tied to the threading model of our code.
- If
doSomething()
,doTheNextThing()
, anddoSomethingELse()
could all do their work on the current thread, we could have something more like this:
Java
doSomething();
try {
String result = doSomethingElse(doTheNextThing());
// TODO
}
catch(Throwable t) {
// TODO
}
Kotlin
doSomething()
try {
val result = doSomethingElse(doTheNextThing())
// TODO
}
catch(t: Throwable) {
// TODO
}
- What we want is to be able to do something like this, while still allowing those functions to do their work on other threads.
Actual Coroutine Syntax
As it turns out, coroutines does just that.
- If
doSomething()
,doTheNextThing()
, anddoTheNextThing()
all employ coroutines, our code invoking those functions could look something like this:
someCoroutineScope.launch {
doSomething()
try {
val result = doSomethingElse(doTheNextThing())
// TODO
}
catch(t: Throwable) {
// TODO
}
}
Key Pieces of Coroutines
Let's look at the following use of coroutines:
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch(Dispatchers.Main) {
println("This is executed before the delay")
stallForTime()
println("This is executed after the delay")
}
println("This is executed immediately")
}
suspend fun stallFortime() {
withContext(Dispatchers.Default) {
delay(2000L)
}
}
- If you run this, you will see:
- "This is executed immediately".
- "This is executed before the delay".
- "This is executed after the delay." after a two-second delay.
The Dependencies
While one element of coroutines (the suspend keyword) ios part of the language, the rest comes from libraries.
- You will need to add these libraries to your project in order to be able to use coroutines.
- Exactly how you add these libraries will depend a lot on your project type and the build system that you are using:
- This book will focus on projects built using
Gradle
.- Such as Android projects in Android Studio.
- Kotlin/ Multiplatform projects in Intellij IDEA.
- This book will focus on projects built using
- This book focuses mostly on version
1.2.2
and1.3- RC
of these dependencies.
Kotlin/JVM and Android
- For an
Android project
, you would want to add a dependency on org.jetbrains.kotlinx:kotlinx-coroutines-android. - This has
transitive dependencies
:- To pull in the core coroutine code.
- Plus it has Android-specific elements.
- If you are using Kotlin/JVM for ordinary Java code, though, the Android code, though, is of no use to you.
- Instead, add a depedency on org.jetbrains.kotlinx:kotlinx-coroutines-core.
Kotlin/JS
- You would want to add a dependency on org.jetbrains.kotlinx:kotlinx-coroutines-core-js.
- Overall, Kotlin/JS has the
least support
for coroutines among the major Kotlin variants. - Mostly, that is because JavaScript itself does not offer first-class threads, but instead relies on
Promise
,web workers
, andsimilar structures
.
Kotlin/Native
- You would want to add a dependency on org.jetbrains.kotlinx:kotlinx-coroutines-core-native.
- Coroutines support for
Kotlin/Native
, right now, isroughly on par
with that of Kotlin/JS.
Kotlin/Common
- In a Kotlin/Multiplatform project, you can depend upon org.jetbrains.kotlinx:kotlinx-coroutines-core-common in any
modules
that are adhering to the Kotlin/Common subset.
The Scope
All coroutine work is managed by a CoroutineScope.
- In the sample code, we are using GlobalScope, which is a global instance of a CoroutineScope.
- Primarily, a CoroutineScope is responsible for:
- Canceling and cleaning up coroutines when the CoroutineScope is no longer needed.
- GlobalScope will set up to support the longest practical lifetime: the
lifetime of the process
that is running the Kotlin code.
- However, while GlobalScope is reasonable for book samples like this, more often you still want to use a scope that is a bit smaller.
For example, if you are using coroutines in an Android app.
- And you are doing
I/O
to populate aUI
.- If the user navigates away from the
activity
orfragment
, you may no longer need that coroutine to be doing its work.- This is why Android, through the Jetpack, offers a range of CoroutineScope implementations that will clean up coroutines when they are no longer useful.
The Builder
- The
launch()
function that we are calling on GlobalScope is acoroutine builder
. - Coroutine builder functions take a
lamda expression
and consider it to be theactual work
to be performed by the coroutine.
The Dispatcher
- Part of the
configuration
that you can provide to a coroutine builder is a dispatcher.- This indicates what
thread pool (or similar structure)
should be used
for executing the code inside of a coroutine.
- This indicates what
- Our code snippet refers to two of these:
- Dispatchers.Default represents a stock pool of threads, useful for general-purpose background work.
- Dispatchers.Main is a dispatcher that is associated with the "main" thread of the environment, such as Android's main application thread.
- By default, a coroutine builder will use Dispatchers.Default, though that default can be overriden in different circumstances, such as different implementations of CoroutineScope.
The suspend Function
The coroutine builders set up blocks of code to be executed by certain thread pools.
Java developers
might draw an analogy to handing a Runnable over to some Executor.- For Dispatchers.Main,
Android developers
might draw an analogy to handing a Runnable over to post() on View or runOnUiThread() on Activity, to have the runnable code be executed on that thread.
However, those analogies are not quite complete.
- In the Runnable scenarios, the unit of work for the designated thread (or thread pool) is the Runnable itself.
- Once the thread starts executing the code in that Runnable, that thread
is now occupied
. - So, if elsewhere we try handing other Runnable objects over, those will wait until the first Runnable is complete.
- In Kotlin, though, we can mark functions with the suspend keyword.
- This tells the coroutines system that it is OK to
suspend execution
of the current block of code and to be feel free to run other code from another coroutine builder if there is any such code to run.
Our
stallForTime()
function has the suspend keyword.
- So, when Kotlin starts executing the code that we provided to
launch()
, when it comes time to callstallForTime()
, Kotlin could elect to executeother coroutines
scheduled for Dispatchers.main. - However, Kotlin will not execute the
println()
statement on the line after thestallForTime()
untilstallForTime()
returns.
suspend function
- Any function marked with suspend needs to be called either:
- From inside a coroutine builder.
- From another function marked with the
suspend
keyword.
- stallForTime() is OK, because we are calling it from code being executed by coroutine builder. [launch()].
- delay() is also OK, because stallForTime() has the suspend keyword, so it is safe to call suspend functions like delay() from within stallForTime().
delay() delays for the requested number of milliseconds.
The Context
The dispatcher that we provide to a coroutine builder is part of a CoroutineContext.
- CoroutineContext: provides a context for executing a coroutine.
- The dispatcher is also part of that context, but there are elements of a CoroutineContext, such as a Job.
withContext()
- The
withContext()
global function
is asuspend function
, so it can only be executed from inside of anothersuspend function
or from inside of a code block executed by acoroutine builder
.
withContext()
takes a different block of code and executes it with a modifiedCoroutineContext
.
- On our snippet, we use
withContext()
to switch to a different thread pool.- Our coroutine starts executing on the main application thread (
Dispatchers.Main
). - In stalllForTime(), though, we execute our delay() call on
Dipatchers.Default
, a courtesy to the withContext() call. - withContext() will block until the code completes, but since it is a suspend function, Kotlin could start work on some other Dispatchers.Main coroutine while waiting for our withContext() call to end.
- Our coroutine starts executing on the main application thread (
Suspending main()
You do not need
GlobalScope.launch()
inside your main() function.
Instead, you can just put the suspend keyword on main() itself.
Now, you can call other suspend functions from main() without having to fuss with the GlobalScope or some other coroutine scope.
- Little production is used directly from a main() function.
- And your choice of Coroutine scope and Coroutine builder are fairly important concepts.
import kotlinx.coroutines.*
suspend fun main() {
println("This is executed before the delay")
stallForTime()
println("This is executed after the delay")
}
suspend fun stallForTime() {
withContext(Dispatchers.Default) {
delay(2000L)
}
}
The Timeline for Events
Sequential Statements
- One coroutine.
- It triggers two delay() calls (by way of two stallForTime() calls).
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch(Dispatchers.Main) {
println("This is executed before the first delay.")
stallForTime()
println("This is executed after the first delay.")
println("This is executed before the second delay.")
stallForTime()
println("This is executed after the second delay.")
}
println("This is executed immediately.")
}
suspend fun stallForTime() {
withContext(Dispatchers.Default) {
delay(2000L)
}
}
This is executed immediately
This is executed before the first delay
This is executed after the first delay
This is executed before the second delay
This is executed after the second delay
Parallel Coroutines
Now, let's divide the work into two separate coroutines, though both are tied to Dispatchers.Main.
- We enqueue two coroutines.
- Kotlin starts executing the first one, but when it hits the
stallForTime()
call, it knows that it can suspend execution of that first coroutine, so Kotlin just waits for our firststallForTime()
call to complete before proceeding. - Now, though, we have
two coroutines
, so when Kotlin encounters our firststallForTime()
, Kotlin can start executing the second coroutine, even though both coroutines are tied to the same thread (Dispatchers.Main).
- So, Kotlin runs the first
println()
from the second coroutine, then hits thestallForTime()
. - At this point,
Kotlin has unblocked coroutine to run
, so it waits for one of the suspend functions to complete. - It can then resume execution of that coroutine.
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch(Dispatchers.Main) {
println("This is executed before the first delay.")
stallForTime()
println("This is executed after the first delay")
}
GlobalScope.launch(Dispatchers.Main) {
println("This is executed before the second delay.")
stallForTime()
println("This is executed after the second delay.")
}
println("This is executed immediately.")
}
suspend fun stallForTime() {
withContext(Dispatchers.Default) {
delay(2000L)
}
}
This is executed immediately
This is executed before the first delay
This is executed before the second delay
This is executed after the first delay
This is executed after the second delay
Cheap, Compared to Threads
In thread-centric programming, we worry about creating too many threads.
- Each thread consumes a chunk of
heap space
. - Plus,
context-switching
between threads consumesCPU time
on top of the actual code execution.- This is why we have
scaling algorithms
for sizing thread pools. - (e.g., twice the number of CPU cores, plus one).
- This is why we have
- However, in Kotlin, coroutines do not declare what thread they run on.
- They declare what dispatcher they run on.
- The dispatcher determines the
threading rules
, which can be:- A single thread.
- Or a constrained thread pool.
import kotlinx.coroutines.*
fun main() {
for(i in 1..100) {
GlobalScope.launch(Dispatchers.Main) {
println("This is executed before delay $i")
stallForTime()
println("This is executed after delay $i")
}
}
}
suspend fun stallForTime() {
withContext(Dispatchers.Default) {
delay(2000L)
}
}
- Here, we execute
100 coroutines
, each delaying fortwo seconds
. - If we were using threads for each of those blocks, we would have
100 threads
, which is far too many. - Instead, we have as many threads as
Dispatchers.Main
uses, and Dispatchers.Main uses onlyone thread
. - Yet, we can do work on the coroutines somewhat in parallel, with Kotlin switching between them when it encounters suspend functions.
So, our code will wind up printing 100 "before delay" messages before printing any of the "after delay" messages.
As we should be able to print 100 messages before the first two-second delay expires.
The History of Coroutines
While Kotlin may be the most popular use of coroutines today, coroutines themselves have been around as programming construct for quite some time.
- They fell out of favor for much of that time, though in favor of a
thread-based concurrency model
. - All of this comes back to "multi-tasking".
Preemptive and Cooperative
The original use of the term "multitasking" in programming was in regards to how programs would appear to perform multiple things ("tasks") at once.
- Two major approaches for multitasking evolved:
- Cooperative.
- Preemptive.
Preemptive
Most modern programmers think in terms of preemptive multitasking, as it is the form of multitasking offered by
processes
andthreads
.
- Here, the code that implements a task is oblivious (mostly) to the fact that there is any sort of multitasking going on.
- Instead, arranging to have tasks run or not is a
function
of the operating system. - The OS schedules processes and threads to run on CPU cores, and the OS switches cores to different processes and threads to ensure that everything that is supposed to be executing gets a chance to do a little bit of work every so often.
Cooperative
- Here, code needs to explicitly "yield".
- And indicate that if there is
some other task needing CPU time
, this would be agood point
in which to switch to that task.
- And indicate that if there is
- Some framework - whether (1)
part of an OS
or (2)within an individual process
- can then switch between tasks at their yield points.- 16-bit Windows programs and the original MAC OS used cooperative multitasking at their core.
- 16-bit Windows programs and the original MAC OS used cooperative multitasking at their core.
- Most often nowadays, cooperative multitasking is handled by some framework inside of an process.
- The OS itself uses preemptive multitasking, to help manage
misbehaving processes
.- Within a process, cooperative multitasking might be used.
For example, 32-bit Windows moved to a preemptive multitasking approach overall, but added fibers as a cooperative multitasking option that could be used within a process.
The Concept of Coroutines
Coroutines originated as the main vehicle for cooperative multitasking.
- Classic implementations of coroutines might literally use the keyword yield .
- Or statement to indicate "this is a good time to switch to some other coroutine, if needed".
- In Kotlin's coroutines, mostly that is handled by calling a suspend function.
- Coroutines, as with any form of cooperative multitasking, requires
cooperation
. - If a coroutine does not yield, then other coroutines cannot run during that period of time.
- This makes coroutines a poor choice of concurrent model between apps, as each app's developers have a tendency to think that their app is more important than is any other app.
- However,
with an app
, developers have to cooperate if their coroutines misbehave otherwise, lest their app crash or encounter other sorts of bugs.
Kotlin's cooroutines are purely an in-app solution for concurrency, and Kotlin relies upon the OS and its preemptive multitasking (processes and threads) for helping to mediate CPU access between several apps.