Part 2 — Coroutine Cancellation and Structured Concurrency
In the first part of this article, we explored the basic concepts of Kotlin coroutines including the launch
and async
coroutine builders. In this follow up article, we will be looking at how to cancel background jobs and structured concurrency in coroutine.
Cancelling a coroutine is quite easy, the Job
object retuned from launch
and async
coroutine builders has a cancel
function that can be invoked to cancel running coroutines. For example:
In the example above, while the body of runBlocking
delays, the launched coroutine prints the string hello
to the console four times and immediately stops running when cancel()
is invoked. You can call cancelAndJoin()
to ensure that the code calling job.cancel()
waits for the completion of the cancelled coroutine before it resumes.
To ensure that your code is cancellable, you need to either check that the coroutine that launched your code is still active with the isActive
property of the current coroutine scope, or call any suspending function from kotlinx.coroutines
like delay
or yield
within your code. These suspending functions throw CancellationException
when the coroutine is cancelled. The example below which is gotten directly from the kotlinx.coroutines
guide on github, shows a coroutine that runs to completion despite the fact that it was cancelled:
The main coroutine delays for 1300ms allowing the body of the while
loop in the launched coroutine to execute. However, the while
loop still runs to completion and two more output gets printed to the console despite the fact that the coroutine from which it is running had been cancelled.
To fix this code, the loop condition can be replaced with an isActive
check or reimplemented with a delay
function from kotlinx.coroutines
Coroutine jobs
have a parent-child relationship. If the Job
of the parent coroutine that launches other coroutine gets cancelled, all children coroutines also get cancelled recursively. For example
Both children coroutines in the above example get cancelled after the third iteration. The parent delays for 1500ms and calls cancel which in turn initiates the cancellation of all children coroutines.
To ensure that a block of code runs to completion without getting cancelled, you can use withContext
passing NonCancellable
object as the context. In the code below, the launched coroutine runs to completion despite the main job getting cancelled after a 100ms delay.
coroutineScope
function can be used to create a custom scope that suspends and only completes when all coroutines launched within that scope complete. If any of the children coroutines within the coroutineScope
throws an exception, all other running sibling coroutines gets cancelled and this exception is propagated up the hierarchy. If the parent coroutine at the top of the hierarchy does not handle this error, it will also be cancelled.
The second suspend function called from the custom coroutineScope
in the snippet above throws an exception after a 250ms delay. This causes both the sibling coroutine and main coroutine to get cancelled. The body of the main coroutine does not continue execution.
supervisorScope
does the opposite of coroutineScope
it also creates a custom scope but if any of the children coroutine within the scope throw an exception, it neither cancel other sibling coroutines nor the parent coroutine. The exception also gets propagated up the hierarchy to the parent coroutine but the parent coroutine does not get cancelled if it does not handle the exception. The exception will be printed to the console as an uncaught exception in this case.
In the snippet above, the first child coroutine within supervisorScope
throws an Exception
on the third repetition but does not cause the second coroutine or parent coroutine to cancel. The exception gets propagated and both the sibling coroutine and parent coroutine run to completion.
In an Android application, to ensure that all launched coroutines get cancelled when the Activity
or Fragment
get destroyed, we can do the following
- Make the
Activity
orFragment
extendCoroutineScope
- override the
coroutineContext
in the scope with a customJob
andDispatcher
- Initialise the
Job
in theonCreate
and cancel the it in theonDestroy
method of theActivity
.
With the above setup, we can call any coroutine builder directly from our Activity
with the guarantee that any running coroutine will be cancelled automatically if our Activity
gets destroyed.
We can also create a custom LifecycleObserver
that implements CoroutineScope
and add this as our Activity lifecycle observer.
For further reading, take a look at the coroutine documentation on coroutine cancellation and the article by Roman Elizarov on Structured Concurrency.
In the third part of this series, we will be looking at how to transmit stream of values with coroutines.
Thanks for reading.
Thanks again to Moyinoluwa Adeyemi and Segun Famisa for proof reading.