Kotlin Coroutines
- 7 minsKotlin Coroutines
Kotlin 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. Lets start with the following very classic code:
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:
Start
Done
Hello
The coroutine is launched with launch
coroutine builder in a context of a CoroutineScope
(in this case GlobalScope
). But what are suspending functions ? coroutine builders ? coroutine contexts ? coroutine scopes?
Suspending functions
Suspending functions are at the center of everything coroutines. A suspending function is simply a function that can be paused and resumed at a later time. They can execute a long running operation and wait for it to complete without blocking.
The syntax of a suspending function is similar to that of a regular function except for the addition of the suspend
keyword. It can take a parameter and have a return type. However, suspending functions can only be invoked by another suspending function or within a coroutine.
Under the hood, suspend functions are converted by the compiler to another function without the suspend keyword, that takes an addition parameter of type Continuation<T>
. The function above for example, will be converted by the compiler to this:
Continuation<T>
is an interface that contains two functions that are invoked to resume the coroutine with a return value or with an exception if an error had occurred while the function was suspended.
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 in 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
Lets use coroutines builders to improve the previous example by introducing runBlocking
:
It is possible to do better ? Yes ! By moving the runBlocking
to wrap the execution of the main function:
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:
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 which 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.
Coroutine (Job) Lifecycle
Let’s apply this to our example:
Using the outer scope’s context
At this point, an option may be to move the inner coroutine to a function:
This works, but there is a more elegant way to achieve this: using suspend
and coroutineScope
[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:
[main] Start
[main] Done
[DefaultDispatcher-worker-1] Hello1
The output though is not the same! why? Here is 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 an CoroutineContext
parameter that can be used to explicitly specify the dispatcher for new coroutine (and other context elements).
Here is 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 thread. Designed for IO-intensive blocking operations.Dispatchers.Unconfined
: Unrestricted to any specific thread or pool. Can be useful for some really special cases, but should not be used in general code.
[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 run inside a scope. A scope can be application wide or specific. But why this is needed ?
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
.
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.
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 cancelled by a single invocation of CoroutineScope.cancel()
in Activity.destroy()
.
Alternatively, we can implement CoroutineScope
interface in this Activity
class, and use delegation with default factory function:
Conclusion
Coroutines are a very good way to achieve asynchonous 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:
Source: