9
Manage Cancellation
Written by Luka Kordić
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
When you initiate multiple asynchronous operations that are dependent on each other, the possibilities of one failing, then leading to others also failing, increases. This often means that the end result won’t be exactly what you expected. Coroutines address this problem and provide cancellation mechanisms to handle this and many other cases.
This chapter will dive deeper into the concepts and mechanics of cancellation in coroutines.
Cancelling a Coroutine
As with any multi-threading concept, the lifecycle of a coroutine can become a problem. You need to stop any potentially long-running background tasks when it is in an inconsistent state in order to prevent memory leaks or crashes. To resolve this, coroutines provide a simple cancelling mechanism.
Job Object
As you’ve seen in Chapter 3: “Getting Started With Coroutines”, when you launch a new coroutine using the launch coroutine builder, you get a Job object as the return value. This Job object represents the running coroutine, which you can cancel at any point by calling cancel()
.
Being Cooperative
In a long-running application, you might need fine-grained control on your background coroutines. For example, a task that launched a coroutine might have finished, and now its result is no longer needed; consequently, its operation can be canceled. This is where cancel()
comes in.
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 10) {
if (System.currentTimeMillis() >= nextPrintTime) {
println("Doing heavy work: $i")
i++
nextPrintTime += 500L
}
}
}
delay(1000)
println("Cancelling coroutine")
job.cancel()
println("Main: now I can quit!")
}
Doing heavy work: 0
Doing heavy work: 1
Doing heavy work: 2
Cancelling coroutine
Main: now I can quit!
Doing heavy work: 3
Doing heavy work: 4
Doing heavy work: 0
Doing heavy work: 1
Doing heavy work: 2
Cancelling coroutine
Main: now I can quit!
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 1000) {
println("Doing heavy work ${i++}")
delay(500)
}
}
delay(1200)
println("Cancelling")
job.cancel()
println("Main: Now I can quit!")
}
Doing heavy work 0
Doing heavy work 1
Doing heavy work 2
Cancelling
Main: Now I can quit!
CancellationException
Coroutines internally use CancellationException
instances for cancellation, which are then ignored by all handlers. They are typically thrown by cancellable suspending functions if the Job of the coroutine is cancelled while it is suspending. It indicates normal cancellation of a coroutine.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// 1
val handler = CoroutineExceptionHandler { _, exception ->
// 6
println("Caught original $exception")
}
// 2
val parentJob = GlobalScope.launch(handler) {
val childJob = launch {
// 4
throw IOException()
}
try {
childJob.join()
} catch (e: CancellationException) {
// 5
println("Rethrowing CancellationException with original cause: ${e.cause}")
throw e
}
}
// 3
parentJob.join()
}
Rethrowing CancellationException with original cause: java.io.IOException
Caught original java.io.IOException
Join, CancelAndJoin and CancelChildren
The Kotlin standard library provides a couple of convenience functions for handling coroutine completion and cancellation.
fun main() = runBlocking {
val job = launch {
println("Crunching numbers [Beep.Boop.Beep]...")
delay(1000L)
}
// waits for job's completion
job.join()
println("main: Now I can quit.")
}
Crunching numbers [Beep.Boop.Beep]…
main: Now I can quit.
fun main() = runBlocking {
val jobOne = launch {
println("Job 1: Crunching numbers [Beep.Boop.Beep]…")
delay(2000L)
}
val jobTwo = launch {
println("Job 2: Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
// waits for both the jobs to complete
joinAll(jobOne, jobTwo)
println("main: Now I can quit.")
}
Job 1: Crunching numbers [Beep.Boop.Beep]…
Job 2: Crunching numbers [Beep.Boop.Beep]…
main: Now I can quit.
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
}
delay(1300L) // delay a bit
println("main: I am tired of waiting!")
// cancels the job and waits for job’s completion
job.cancelAndJoin()
println("main: Now I can quit.")
}
0. Crunching numbers [Beep.Boop.Beep]…
1. Crunching numbers [Beep.Boop.Beep]…
2. Crunching numbers [Beep.Boop.Beep]…
main: I am tired of waiting!
main: Now I can quit.
fun main() = runBlocking {
val parentJob = launch {
val childOne = launch {
repeat(1000) { i ->
println("Child Coroutine 1: " +
"$i. Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
}
// Handle the exception thrown from `launch`
// coroutine builder
childOne.invokeOnCompletion { exception ->
println("Child One: ${exception?.message}")
}
val childTwo = launch {
repeat(1000) { i ->
println("Child Coroutine 2: " +
"$i. Crunching numbers [Beep.Boop.Beep]…")
delay(500L)
}
}
// Handle the exception thrown from `launch`
// coroutine builder
childTwo.invokeOnCompletion { exception ->
println("Child Two: ${exception?.message}")
}
}
delay(1200L)
println("Calling cancelChildren() on the parentJob")
parentJob.cancelChildren()
println("parentJob isActive: ${parentJob.isActive}")
}
Child Coroutine 1: 0. Crunching numbers [Beep.Boop.Beep]…
Child Coroutine 2: 0. Crunching numbers [Beep.Boop.Beep]…
Child Coroutine 1: 1. Crunching numbers [Beep.Boop.Beep]…
Child Coroutine 2: 1. Crunching numbers [Beep.Boop.Beep]…
Child Coroutine 1: 2. Crunching numbers [Beep.Boop.Beep]…
Child Coroutine 2: 2. Crunching numbers [Beep.Boop.Beep]…
Calling cancelChildren() on the parentJob
parentJob isActive: true
Child One: Job was canceled
Child Two: Job was canceled
Timing Out
Long-running coroutines are sometimes required to terminate after a set time has passed. While you can manually track the reference to the corresponding Job and launch a separate coroutine to cancel the tracked one after a delay, the coroutines library provides a convenience function called withTimeout. To see it in action, open the WithTimeoutExample.kt file in the starter project, and use the code below.
fun main() = runBlocking {
withTimeout(1500L) {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]...")
delay(500L)
}
}
}
0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1500 MILLISECONDS
...
fun main() = runBlocking {
try {
withTimeout(1500L) {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]...")
delay(500L)
}
}
} catch (e: TimeoutCancellationException) {
println("Caught ${e.javaClass.simpleName}")
}
}
0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Caught TimeoutCancellationException
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("$i. Crunching numbers [Beep.Boop.Beep]...")
delay(500L)
}
"Done" // will get canceled before it produces this result
}
// Result will be `null`
println("Result is $result")
}
0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Result is null
Key Points
- You can use
cancel()
onJob
instances to cancel a coroutine. - Always make sure that your code is cooperative with cancellation.
- All functions from the standard library support cancellation out of the box.
- When the parent coroutine is canceled, all of its children are recursively canceled, too.
- Coroutines manage cancellation internally by using CancellationException.
- CancellationException is not printed to the console/log by the default uncaught exception handler.
- Using the withTimeout function, you can terminate a long-running coroutine after a set time has elapsed.
Where to Go From Here?
Being able to cancel an ongoing task is almost always required. The cycle of starting a coroutine and canceling it when an exception is thrown or when the business logic demands it is part of some of the common patterns in programming. Coroutines in Kotlin were built keeping that in mind since the very beginning.