We have already looked at coroutines briefly in the context of Android. These notes will extend upon that and look at some additional features of coroutines.
To revise: Kotlin allows you to achieve the same effect as threads with coroutines. A coroutine is a function that can run in either the foreground or the background, can switch between foreground and background without having to write asynchronous code, and can be suspended (paused) to allow another operation to take place.
We also saw that coroutines must be launched from a scope. The scope represents what the coroutine is launched from, e.g. a console-mode program or an Activity
. The coroutine has a lifetime equal to the lifetime of its scope. For example a coroutine launched from the Android lifecycleScope
means the coroutine has the scope of the parent component (e.g. activity or fragment) and will stop when the parent is destroyed.
For this topic we will use command-line Kotlin. You can use the
IntelliJ IDEA IDE to try out these examples, but if you do, you will need to add the coroutines library as a dependency to build.gradle
(choose a Gradle project) because it's separate to the Kotlin standard library.
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")If you have access to a machine with the command-line, you can also install Kotlin standalone.
There are some exercises here. As well as doing the exercises, try out the examples to get a feel of what is going on.
Here is an example of a basic Kotlin program which launches a coroutine. The coroutine is launched from the global scope, which is the scope of the running program.
fun main() { GlobalScope.launch { for(i in 0..100) { println(i) } } println("Not in the coroutine") }This code is launching a coroutine which counts to 100. The code inside
launch
will run in the background, so
Not in the coroutine will appear before the launch
block
has finished. But actually in this example, the main()
function does not wait for the coroutine to finish, so you never see the output of the for
loop. This is because a coroutine launched from a given scope will
terminate when its parent scope terminates. Here, the parent
scope is the GlobalScope
- the scope of the program - and thus
will terminate when the program terminates.
If you add
Thread.sleep(1000)after the "Not in the coroutine" println, then the GlobalScope will pause 1000 milliseconds and will allow the coroutine to finish.
The other approach to the above would be to tell the GlobalScope to wait for the coroutine to finish. We could use join()
for this.
For example, would this code work?
fun main() { val coroutine = GlobalScope.launch { for(i in 0..100) { println(i) } } println("Not in the coroutine") coroutine.join() }Even though it looks sensible, this will not work, you will get a compiler error informing you that
join()
can only be called in another coroutine. The problem is, our main()
is not a coroutine; it is just a
regular function. However, You can turn main()
into a coroutine by
assigning it to the result of a runBlocking
call.
runBlocking
launches a coroutine and "blocks the current thread until all tasks inside that coroutine are finished" (Leiva).
Here, this means that the main thread
blocks (cannot do any other tasks) until the coroutine launched with
runBlocking
has finished. This will mean the program will only complete when the runBlocking
coroutine has finished.
It's important to note that runBlocking()
will create a new scope, separate from the global scope.
fun main() = runBlocking { val coroutine = GlobalScope.launch { for(i in 0..100) { println(i) } } println("Not in the coroutine") coroutine.join() }This will work as expected, i.e the
join()
call will wait for the inner coroutine to complete before finishing. We can legally call join()
here, as it's being called inside a coroutine (the one launched with runBlocking
).
So we will see:
Not in the coroutine 0 1 ... 100as the output.
Within a coroutine, we call tell it to delay by a certain number of milliseconds.
fun main() = runBlocking { val coroutine = GlobalScope.launch { delay(1000) // delay the start of the counter for one second for(i in 0..100) { delay(100) // delay for 100ms each time the loop runs println(i) } } println("Not in the coroutine") coroutine.join() }This will delay execution in the coroutine by 100 milliseconds each iteration of the loop.
delay()
is non-blocking, ie. the delay in the coroutine
will not delay other strands of execution; for example, here, the coroutine
launched with GlobalScope.launch
will block, but the main()
coroutine will not.
main()
coroutine. One
should count
from 0 to 50 in steps of 2 (add step 2
to your for loop), pausing
for 100 milliseconds each time. The other should count from 100 to 1000 in steps
of 100 (add step 100
to your for loop), pausing for one second each time. Write a "finished" message in your main()
coroutine when
both coroutines have finished.suspend
and allow code to be suspended within them.
withContext() works because it is a suspending function itself.
What if we wanted to do the loop coroutine (with delay, as in the previous
example) from a function from a coroutine? Would this work?
fun main() = runBlocking { val coroutine = GlobalScope.launch { doLoop() } println("Not in the coroutine") coroutine.join() } fun doLoop() { for(i in 0..100) { delay(100) println(i) } }This would cause a compilation error, because
delay()
can only be called within a coroutine.Even though doLoop()
is called from
within the coroutine launched with GlobalScope.launch()
,
it will not work as doLoop
has to be declared as a
suspend function.
A suspend function is a function which is capable of suspending
a coroutine, as delay() does here. So we need to rewrite our code as follows:
fun main() = runBlocking { val coroutine = GlobalScope.launch { doLoop() } println("Not in the coroutine") coroutine.join() } suspend fun doLoop() { for(i in 0..100) { delay(100) println(i) } }
Consider this example:
fun main() = runBlocking{ GlobalScope.launch { for(i in 0..10) { delay(100) println(i) } } println("Not in the coroutine") }As we have seen, this example will not wait for the loop to finish, as we did not use
join()
. But what does the
GlobalScope actually mean? It is the
global scope, i.e. the scope of the running program. As we have seen, a coroutine launched in a given scope will be terminated when its parent scope (here the running program) is terminated. So when the program finishes, the coroutine will terminate even if the loop hasn't finished iterating yet.
Can we deal with this without using join()? Yes -
in fact we can - we can launch the coroutine using plain
launch
, not GlobalScope.launch
.
This is because, if the scope is not specified, the given coroutine
will be launched in the scope of its parent coroutine. As the parent
coroutine is the main() coroutine launched with runBlocking
, it
will mean that the child coroutine (the one which does the loop) will
also execute in the runBlocking
scope.
So it won't finish until the main()
coroutine has finished.So if we
do :
fun main() = runBlocking{ launch { for(i in 0..10) { delay(100) println(i) } } println("Not in the coroutine") }the
runBlocking
scope will wait for the coroutine to finish before
finishing itself because the coroutine is now running in the scope of runBlocking rather than the program scope.
launch() is a method of CoroutineScope therefore it must be called within a coroutineScope or be called from GlobalScope.launch. It takes a block of code to run - a function which will be run as the coroutine.
main()
a suspend functionAs an alternative you can make main()
a suspend function, which allows you to launch coroutines in the GlobalScope
(but unlike runBlocking()
, does not give you a separate scope):
suspend fun main() { GlobalScope.launch { // ... } }
What is actually returned when we launch a coroutine with launch()
? We are returned a Job object. This can be used to control the
coroutine, for example, cancel it. The join() method you've seen already is a
method of Job; this will cause the outer coroutine (the one launched with runBlocking()
to wait until the child coroutine that the job refers to has finished.
One of the key things that coroutines allow is concurrency. We can launch several coroutines in parallel by launching multiple jobs. The code below shows this and also shows the use of joinAll()
to wait for all coroutines in a list to complete. Note how we write a function launchJob()
to launch a coroutine from a given scope.
import kotlinx.coroutines.* fun main() = runBlocking { val jobs = listOf( launchJob(GlobalScope, 0, 10, 1, 1000L), launchJob(GlobalScope, 0, 20, 2, 4000L) ) jobs.joinAll() println("All jobs finished") } fun launchJob(scope: CoroutineScope, start: Int, end: Int, step: Int, delay: Long): Job { return scope.launch { for (i in start..end step step) { // Use formatted text, %d means whole number (int or long), %4d means use a field 4 characters wide println("Counting from %4d to %4d, step %2d, delay %4d ------- %d".format(start, end, step, delay, i)) delay(delay) } println("Counting from %4d to %4d, step %2d, delay %4d ------- COMPLETED!").format(start, end, step, delay)) } }
fun main() = runBlocking{ launch { for(i in 0..10) { delay(1000) println(i) } } println("Not in the coroutine") }Modify the example so that the main coroutine waits for 5 seconds after launching the loop coroutine, and then cancels the loop coroutine. Job has a
cancel()
method. Does it still count to 10?runBlocking
coroutine's own scope, rather than the GlobalScope
. (Hint: runBlocking
takes a lambda function, with the scope as its single parameter). If you do this, can you remove anything from the code?