Kotlin Coroutines Basics
- 11 minsKotlin v1.3 was released, bringing coroutines for asynchronous programming. This article is a quick introduction to the core features of kotlinx.coroutines
.
Let’s say the objective is to say hello asynchronously. Let’s start with the following classic code snippet:
fun main() {
println("Start")
Thread {
Thread.sleep(3000L)
println("Hello")
}.start()
println("Done")
}
Start
Done
Hello
What happened within the 3 seconds when the created Thread
was sleeping? The answer: nothing! The thread was occupying memory without being used! This is when coroutines the light-weight threads kick in!
First Coroutine
The following is a basic way to migrate the previous example to use coroutines:
fun main() {
println("Start")
// Start a coroutine
GlobalScope.launch {
delay(1000L)
println("Hello")
}
Thread.sleep(2000L)
println("Done")
}
Start
Done
Hello
The coroutine is launched with launch
coroutine builder in a context of a CoroutineScope
(in this case GlobalScope
). But what are coroutine builders? coroutine contexts? coroutine scopes?
Coroutine Builders
Coroutine builders are simple functions to create a new coroutine; the following are the main ones:
launch
: used for starting a computation that isn’t expected to return a specific result.launch
starts a coroutine and returns aJob
, which represents the coroutine. It is possible to wait until it completes by callingJob.join()
.async
: likelaunch
it starts a new coroutine but returns aDeferred
* object instead: it stores a computation, but it defers the final result; it promises the result sometime in the_future_.runBlocking
: used as a bridge between blocking and non-blocking worlds. It works as an adaptor starting the top-level main coroutine and is intended primarily to be used in main functions and tests.withContext
: calls the given code with the specified coroutine context, suspends until it completes, and returns the result. An alternative (but more verbose) way to achieve the same thing would be:launch(context) { … }.join()
.
* Deffered
is a generic type which extends Job
.
Building Coroutines
Let’s use coroutines builders to improve the previous example by introducing runBlocking
:
fun main() {
println("Start")
// Start a coroutine
GlobalScope.launch {
delay(1000L)
println("Hello")
}
runBlocking {
delay(2000L)
}
println("Done")
}
It is possible to do better? Yes! By moving the runBlocking
to wrap the execution of the main function:
fun main() = runBlocking {
println("Start")
GlobalScope.launch {
delay(1000L)
println("Hello")
}
delay(2000L)
println("Done")
}
But wait a minute, the initial goal of having delay(2000L)
was to wait for the coroutine to finish! Let’s explicitly wait for it then:
fun main() = runBlocking {
println("Start")
val job = GlobalScope.launch {
delay(1000L)
println("Hello")
}
job.join()
println("Done")
}
Structured concurrency
In the previous example, GlobalScope.launch
has been used to create a top-level “independent” coroutine. Why “top-level”? Because GlobalScope
is used to launch coroutines that are operating on the whole application lifetime. “Structured concurrency” is the mechanism providing the structure of coroutines which gives the following benefits:
- The scope is generally responsible for children coroutines, and their lifetime is attached to the lifetime of the scope.
- The scope can automatically cancel children coroutines in case of the operation canceling or revoke.
- The scope automatically waits for completion of all the children coroutines.
Let’s apply this to our example:
fun main() = runBlocking {
println("Start")
// Start a coroutine (Child coroutine of runBlocking)
launch { // or `this.launch`.
delay(1000L)
println("Hello")
}
println("Done")
}
Using the outer scope’s context
At this point, an option may be to move the inner coroutine to a function:
fun main() = runBlocking {
println("Start")
hello(this)
println("Done")
}
fun hello(scope: CoroutineScope) { // or as extension function
scope.launch {
delay(1000L)
println("Hello")
}
}
This works, but there is a more elegant way to achieve this: using suspend
and coroutineScope
fun main() = runBlocking {
println("Start")
hello()
println("Done")
}
suspend fun hello() = coroutineScope {
launch {
delay(1000L)
println("Hello")
}
}
[main] Start
[DefaultDispatcher-worker-1] Hello1
[main] Done
The new scope created by coroutineScope
inherits the context from the outer scope.
CoroutineScope extension vs suspend
The previous example (using suspend
) can be rewritten using CoroutineScope
extension:
fun main() = runBlocking {
log("Start")
hello() // not suspendable, no waiting!
log("Done")
}
private fun CoroutineScope.hello() = launch(Dispatchers.Default) {
delay(1000L)
log("Hello1")
}
[main] Start
[main] Done
[DefaultDispatcher-worker-1] Hello1
The output is not the same! why? Here are the rules:
suspend
: function do something long and waits for it to complete without blocking.- Extension of
CoroutineScope
: function launch new coroutines and quickly return without waiting for them.
Coroutine Context and Dispatchers
Coroutines always execute in some CoroutineContext
. The coroutine context is a set of various elements. The main elements are the Job
of the coroutine and its CoroutineDispatcher
.
Dispatchers
CoroutineContext
includes a CoroutineDispatcher
that determines what thread or threads the corresponding coroutine uses for its execution. Coroutine dispatcher can confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined.
Coroutine builders launch
, async
and withContext
accept a CoroutineContext
parameter that can be used to explicitly specify the dispatcher for new coroutine (and other context elements).
Here are various implementations of CoroutineDispatcher
:
Dispatchers.Default
: the default dispatcher, that is used when coroutines are launched inGlobalScope
. Uses shared background pool of threads, appropriate for compute-intensive coroutines.Dispatchers.IO
: Uses a shared pool of on-demand created threads. Designed for IO-intensive blocking operations.Dispatchers.Unconfined
: Unrestricted to any specific thread or pool. Can be useful for some special cases, but should not be used in general code.
fun main() = runBlocking {
log("Start")
hello()
log("Done")
}
private suspend fun hello() = coroutineScope {
launch {
// context of the parent, main runBlocking coroutine
println("[${Thread.currentThread().name}] from parent dispatcher")
}
launch(Dispatchers.Default) {
// will get dispatched to DefaultDispatcher
println("[${Thread.currentThread().name}] Dispatchers.Default")
}
launch(Dispatchers.IO) {
// will get dispatched to IO
println("[${Thread.currentThread().name}] Dispatchers.IO")
}
launch(Dispatchers.Unconfined) {
// not confined -- will work with main thread
println("[${Thread.currentThread().name}] Dispatchers.Unconfined")
}
}
[main] Start
[DefaultDispatcher-worker-2] Dispatchers.Default
[DefaultDispatcher-worker-1] Dispatchers.IO
[main] Dispatchers.Unconfined
[main] from parent dispatcher
[main] Done
(Dispatcher.IO
dispatcher shares threads with Dispatchers.Default
)
Coroutine Scope
Each coroutine runs inside a scope. A scope can be application-wide or specific. Contexts and jobs lifecycles are often tied to objects who are not coroutines (Android activities for example). Managing coroutines lifecycles can be done by keeping references and handling them manually. However, a better approach is to use CoroutineScope
.
The best way to create a CoroutineScope
is using:
CoroutineScope()
: creates a general-purpose scope.MainScope()
: creates scope for UI applications and usesDispatchers.Main
as default dispatcher.
fun main() {
println("Start")
val activity = Activity()
activity.doLotOfThings()
Thread.sleep(2000)
activity.destroy()
println("Done")
}
private class Activity {
private val mainScope = CoroutineScope(Dispatchers.Default)
fun doLotOfThings() {
mainScope.launch {
repeat(1_000) {
delay(400)
doThing()
}
}
}
private fun doThing() {
println("[${Thread.currentThread().name}] doing something...")
}
fun destroy() {
mainScope.cancel()
}
}
Start
[DefaultDispatcher-worker-1] doing something...
[DefaultDispatcher-worker-3] doing something...
[DefaultDispatcher-worker-2] doing something...
[DefaultDispatcher-worker-2] doing something...
Done
Only the first four coroutines had printed a message and the others were canceled by a single invocation of CoroutineScope.cancel()
in Activity.destroy()
.
Alternatively, we can implement CoroutineScope
interface in this Activity
class, and use delegation with the default factory function:
fun main() {
println("Start")
val activity = Activity()
activity.doLotOfThings()
Thread.sleep(2000)
activity.destroy()
println("Done")
}
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun doLotOfThings() {
launch {
repeat(1_000) {
delay(400)
doThing()
}
}
}
private fun doThing() {
println("[${Thread.currentThread().name}] doing something...")
}
fun destroy() {
cancel()
}
}
Conclusion
Coroutines are a very good way to achieve asynchronous programming with kotlin.
The following is an (over)simplified diagram of coroutines structure while keeping in mind each Element
is a CoroutineContext
by its own:
For more advanced topics like composing suspending functions, exception handling, and supervision, the main coroutines guide is the way to go!
Tips
-Dkotlinx.coroutines.debug
as VM parameter for debugging.CoroutineName
as a parameter to coroutine builders for debugging purposes.- Combining context element can be using
+
operator:launch(Dispatchers.Default + CoroutineName("test")) { … }
.