Originally written by Florina Muntenescu

Cancellation and Exceptions in Coroutines (Part 2)

Translator: Jingpingcheng

Calling cancel

When launching multiple coroutines, it’s just too much trouble to just cancel them one by one. We can cancel all child coroutines by calling scope’s cancel() method:

// assume we have a scope defined for this layer of the app
valJob1 = scope.launch {... }valJob2 = scope.launch {... } scope.cancel()Copy the code

Cancelling the scope cancels its children But sometimes you may wish to cancel one of the coroutines without affecting the other coroutines:

// assume we have a scope defined for this layer of the app
valJob1 = scope.launch {... }valJob2 = scope.launch {... }// First coroutine will be cancelled and the other one won’t be affected
job1.cancel()
Copy the code

A Cancelled Child Doesn’t affect other Siblings Coroutines cancels by throwing A special CancellationException. You can pass a CancellationException instance in the cancel() method to record detailed exception information:

fun cancel(cause: CancellationException? = null)
Copy the code

The cancel() method also creates a CancellationException instance by default if you don’t pass it in: CancellationException

public override fun cancel(cause: CancellationException?).{ cancelInternal(cause ? : defaultCancellationException()) }Copy the code

You can handle cancellation of a Coroutine by catching a CancellationException. The cancellation of a child job is notified to its parent job by throwing this exception. If the child job throws a CancellationException, the Parent job does not need to handle the exception. ⚠️Once you cancel a scope, you won’t be able to launch new coroutines in the cancelled scope. In Android development, if you already use the KTX extension, you don’t need to create a custom CoroutineScope or manually handle cancellations of Coroutines. For example, when you use ViewModel, we provide an extended property of ViewModel, viewModelScope. LifecycleScope is used when you want to create a life-cycle aware scope. Both viewModelScope and lifecycleScope automatically cancel executing coroutines at the right time. Coroutines in Android ViewModelScope

The translator’s note: For special cases where you don’t want Coroutines to be automatically cancelled, such as when you want to send logs to the backend server, or when something important needs to be done, use GlobalScope.launch {}, but be aware that it has a lifetime as long as your application. Use caution to prevent memory leaks. There is another way to continue executing suspended code while coroutines have been cancelled, as described later in this article.

Why isn’t my coroutine work stopping?

Just because you call cancel doesn’t mean your Coroutines suddenly stop executing the rest of the code. For example, if you call cancel while reading files in bulk, your coroutine will not stop working immediately.

Note: This is the same as for Thread interruption in Java. Calling threadInterrupt () is followed by checking isInterrupted() to gracefully handle Thread exit. Let’s look at an example:

import kotlinx.coroutines.*
 
fun main(args: Array<String>) = runBlocking<Unit> {
   val startTime = System.currentTimeMillis()
    val job = launch (Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("Hello ${i++}")
                nextPrintTime += 500L
            }
        }
    }
    delay(1000L)
    println("Cancel!")
    job.cancel()
    println("Done!")}Copy the code

The input result is:

Hello 0
Hello 1
Hello 2
Cancel!
Done!
Hello 3
Hello 4
Copy the code

Hello 3 and Hello 4 are printed after job.cancel() is executed. Even though the coroutine is Cancelling when job.cancel() is called, the rest of the code continues to execute until all work is done.

Cancellation of coroutine code needs to be cooperative!

Cancellation of coroutine code needs to be cooperative!

Coroutine exits must be collaborative, and you need to check the status of coroutine periodically:

val job = launch {
    for(file in files) {
        // TODO checks here to see if the Job has been cancelled and if the rest of the code needs to be executed
        readFile(file)
    }
}
Copy the code

All suspend methods under the Kotlinx. coroutines package are cancelable: withContext, delay, and so on. When you use these cancellable methods, you do not need to check to see if coroutines have already cancelled or thrown a CancellationException. However, if you don’t use these methods, you’ll need to deal with coroutines cancellations yourself:

  • Check the statusjob.isActiveorensureActive()
  • Or useyield()methods

The translator’s note: The yield method calls Context.checkcompletion (), and both ensureActive() and yield() check whether the state of the Job is still active. If not, a CancellationException is thrown to terminate coroutines:

public fun Job.ensureActive(a): Unit {
    if(! isActive)throw getCancellationException()
}

internal fun CoroutineContext.checkCompletion(a) {
    val job = get(Job)
    if(job ! =null && !job.isActive) throw job.getCancellationException()
}
Copy the code

Checking for job’s active state

Going back to the previous example, we can check if the coroutine has been canceled by calling the job’s isActive property in the while loop:

// Since we're in the launch block, we have access to job.isActive
while (i < 5 && isActive)
Copy the code

So the code in the while loop will execute only if the job is still active, and we can pass the while loop! IsActive criteria to perform custom tasks, such as logging Job cancelled. Another way to do this is to use the helper method ensureActive() inside the while loop

while (i < 5) {ensureActive ()... }Copy the code

The advantage of this is that you do not need to access the isActive attribute directly. The ensureActive() method will raise a CancellationException when the Job is cancelled to ensure that the rest of the code will not be executed. But using ensureActive() will lose the flexibility of code control in certain situations, such as passing the which loop at the end! IsActive Condition to log the cancellation of the Job.

Let other work happen using yield()

When should we use yield()?

  1. Heavy CPU computing tasks.
  2. A task that may exhaust thread pool resources.
  3. We want threads in the Thread pool to work together better than when we add more threads to the thread pool.

In this case, you can use yield() to periodically check the state of the Job, just like the ensureActive() method in the example above.

Job.join vs Deferred.await cancellation

There are two ways to get the return value of a Coroutine:

  1. job.join()methods
  2. async{}.wait()Method,async{}Returns a Job of type Deferred

The job.join() method suspends the coroutine until the job completes, and when used with job.cancel produces the following effect:

  • First calljob.cancelAfter the calljob.join(), the pending job will not be executed.

If you’re calling job.Cancel then job.join, the coroutine will suspend until the job is completed. Job.join () checks whether the job is active or not. If job.join() is placed after job.cancel, the job will not execute the following code.

// The relevant implementation of join is in jobsupport.kt
public final override suspend fun join(a) {
    if(! joinInternal()) {// fast-path no wait
        coroutineContext.checkCompletion()
        return // do not suspend
    }
    return joinSuspend() // slow-path wait
}
Copy the code
  • First calljob.join()After the calljob.cancelIf so, cancel is invalid because the Coroutine has already completed.

Let’s examine the effect of using Deferred jobs created with async with cancel() :

valDeferred = async {... } deferred.cancel()val result = deferred.await() // throws JobCancellationException!
Copy the code
  • First callcancel()Call after methodawait()Method can throw an exception: directly JobCancellationException: Job was cancelled
  • First callawait()Call after methodcancel()Method, nothing will happen because the Coroutine has already been executed.

Handling cancellation side effects

If you want to do something after a Coroutine cancels: close a resource in use, log a cancellation or run some cleanup code, etc.

Check for ! isActive

You can periodically check the isActive property, and once out of the while loop, clean up:

while (i < 5 && isActive) {
    // print a message twice a second
    if(...). Println (" Hello ${i++} ") nextPrintTime +=500L}}// the coroutine work is completed so we can cleanupPrintln (" the Clean up!" )Copy the code

Try catch finally

Since coroutine cancelling throws a CancellationException, we can catch the exception and clean up in the finally block:

val job = launch {
   try {
      work()
   } catchE: CancellationException {println (" Work cancelled!" )}finally{println (" the Clean up!" ) } } delay(1000LPrintln (" Cancel!" ) the job. The cancel () println (" Done!" )Copy the code

Note that once the coroutine is in the canceled state, the code to be executed next does not suspend. A coroutine in the cancelling state is not able to suspend! If, in a clean up operation, you want to continue executing code that can be suspended, you can use the CoroutineContext NonCancellable. It maintains the coroutine cancel state until the pending code executes:

val job = launch {
   try {
      work()
   } catchE: CancellationException {println (" Work cancelled!" )}finally {
      withContext(NonCancellable){
         delay(1000L) // or some other suspend fun Println (" Cleanup done!" ) } } } delay(1000LPrintln (" Cancel!" ) the job. The cancel () println (" Done!" )Copy the code

suspendCancellableCoroutine and invokeOnCancellation

Using suspendCancellableCoroutine and continuation. InvokeOnCancellation to handle coroutines callback (translator note: similar to when there is a change for coroutines life cycle to add the callback function)

suspend fun work(a) {
   return suspendCancellableCoroutine { continuation ->
       continuation.invokeOnCancellation { 
          // do cleanup
       }
   // rest of the implementation
}
Copy the code

Translator note: suspendCancellableCoroutine can be through the job. The cancel () to cancel.

Conclusion: Canceling Coroutines is a science, please use viewModelScope or lifecycleScope.