Home Android & Kotlin Books Kotlin Coroutines by Tutorials

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 scrambled 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() on Job 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.