When I first learned about Kotlin coroutines, I was having a hard time identifying the specific behavior of all keywords: launch
, withContext
, async
, etc...
Is my coroutine suspended ? How can I wait many of them at one point ? Where are they executing ?
Quickly, I decided to sum up some key basic behaviors to help me with that, and that's what I will share today ! This post is aimed at developers who are not using coroutines right now, or are using it but need a quick refresh on some basic behaviors.
What is a coroutine ?
According to the offical documentation, a coroutine is essentially a (very) light-weight thread.
It enables asynchronous operations without the need of callbacks, Promise
, or Future
concepts. Write imperative code, run concurrent operations.
Snippets setup
Let's declare some context for our coroutines snippets:
private val job = Job()
private val customScope = CoroutineScope(job + Dispatchers.Default)
private suspend fun fakeHardWork(tag: String): String {
println(tag)
delay(4000)
return "I'm exhausted"
}
- A
Job
is responsible for holding a reference to a coroutine. With that reference you can.start()
the coroutine if it was not started, wait for its completion with.join()
or cancel it with.cancel()
-
customScope
is aCoroutineScope
composed of thejob
and aCoroutineDispatcher
(and it can be much more). Yes, I added aCoroutineDispatcher
to aJob
. That's because they both are aCoroutineContext
and they can be merged together. -
fakeHardWork()
is, as the name can suggest, asuspend
function faking a 4 seconds task
I did not get into the details of these objects that are part of the coroutine's vocabulary. I encourage you to read the documentation about all of them, or find some posts describing them.
Quick look at CoroutineDispatcher
The CoroutineDispatcher
is responsible for describing where the coroutine must be executed. Some dispatchers are provided by the core
library with these properties :
-
Dispatchers.Default
which serves for basic background task. -
Dispatchers.IO
offers a thread pool optimized for IO tasks (only available for the JVM). -
Dispatchers.Unconfined
if the thread of execution does not matter.
Finally, Dispatchers.Main
property is a special dispatcher that is allocated to main thread operations. Such tasks as manipulating the UI are often required to be executed on this dispatcher (Android crashes the app if that's not the case for example).
Using the
Dispatchers.Main
property without providing a concrete implementation in the classpath of your application will throw anIllegalStateException
.kotlinx-coroutines-android
is the provider of theMain
dispatcher for Android.
Launching a coroutine
fun newCoroutine() =
customScope.launch { // starts a new coroutine 'C1'
fakeHardWork("working") // this suspends C1 until the function completes
println("done")
}
Theoretical output:
00s:0000ms : working
04s:0000ms : done
This piece of code is quit straighforward. We launch a new coroutine inside the customScope
. Since this scope has been initialized with Dispatchers.Default
, it's executing on the corresponding thread pool.
launch
creates and executes a new coroutine and must be called on an existingCoroutineScope
.
Switching Dispatchers
fun switchDispatchers() =
customScope.launch { // starts a new coroutine 'C1'
fakeHardWork("work from Dispatchers.Default") // same as above, suspends C1
// switch Dispatcher, still suspends C1
withContext(Dispatchers.IO) {
fakeHardWork("work from Dispatchers.IO")
}
println("done on Dispatchers.Default")
}
Theoretical output:
00s:0000ms : work from Dispatchers.Default
04s:0000ms : work from Dispatchers.IO
08s:0000ms : done on Dispatchers.Default
Here, withContext
changes the execution context of the block in parameter. The second call to fakeHardWork()
is executed on Dispatchers.IO, and withContext
is still suspending C1.
withContext
merges the current CoroutineContext
it has been launched from (here customScope
's context), with the context you passed in parameter.
withContext
does not create a new coroutine and suspends its caller until its block is executed.
Parallel execution inside a coroutine
fun parallelWorkInCoroutine() =
customScope.launch { // starts a new coroutine C1
// fire a new coroutine C2. C1 is not suspended
val work_1 = async { fakeHardWork("w_1 from Dispatchers.Default") }
// fire a new coroutine C3 on another Dispatcher. C1 is not suspended
val work_2 = async(Dispatchers.IO) { fakeHardWork("w_2 from Dispatchers.IO") }
// await() calls suspend C1
val result = work_1.await() + work_2.await()
println(result)
}
Theoretical output:
00s:0000ms : w_1 from Dispatchers.Default
00s:0010ms : w_2 from Dispatchers.IO
04s:0010ms : I'm exhaustedI'm exhausted
Now this is interesting. async
is a coroutine builder. Unlike withContext
, it actually creates and starts a new coroutine while also immediatly returning a Deferred<T>
(T
being the return type of the block passed to async
, a String
in our example).
We can then call await()
on this object to effectively suspend the caller coroutine (C1 in the snippet) and get back the String
produced by the blocks passed to async
.
Here result
equals to "I'm exhaustedI'm exhausted"
.
With this, we could await many Deffered
and then execute something only when all of them complete.
async
creates and starts a new coroutine. The returnedDeferred
allows us to suspend the caller coroutine and get the produced value.
Wrapping it up
I described very few behaviors about coroutines, there's so much more to discover and to learn about them ! But I find these points to be good tools to start manipulating coroutines and learn step by step.
I'll probably write later about other more advanced concepts. For now, if you were asking yourself if coroutines are worth a try, go ahead !
More starting resources
If you are curious, this great talk is a perfect introduction to Coroutines from the creator, Roman Elizarov.
Top comments (0)