In everyday development, we all know that we should avoid unnecessary task processing to save memory space and power usage on the device — the same principle applies to coroutines. You need to control the life cycle of coroutines and cancel them when they are not needed, which is what structured concurrency advocates. Read on to learn more about the ins and outs of coroutine cancellations.

⚠ ️ in order to be able to better understand about the contents of this article, it is recommended that you first read the first article in this series: cancel and exception of coroutines | core concept is introduced.

Call the cancel method

When starting multiple coroutines, it can be a headache to either track the coroutine state or cancel each coroutine individually. However, we can solve this problem by simply taking the entire scope involved in starting the coroutine, because this cancels all created child coroutines.

Val job1 = scope.launch {... } val job2 = scope.launch {... } scope.cancel()Copy the code

Cancelling a scope cancels its subcoroutine

Sometimes, you might just need to cancel one of the coroutines, such as a user entering an event to cancel an ongoing task in response. As shown in the code below, calling job1.cancel ensures that only the specific coroutines associated with job1 are cancelled, without affecting the remaining sibling coroutines to continue working.

// Suppose we have defined a scope

valJob1 = scope.launch {... }valJob2 = scope.launch {... }// The first coroutine will be cancelled, while the other is unaffected
job1.cancel()
Copy the code

Cancelled subcoroutines do not affect the remaining sibling coroutines

Coroutines handle cancellation by throwing a special CancellationException. You can provide more details about the cancellation by passing in an instance of CancellationException when calling.cancel, which is signed as follows:

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

If you do not build a new CancellationException instance and pass it in as a parameter, a default CancellationException is created (see the full code).

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

Once a CancellationException is thrown, you can use this mechanism to handle cancellation of coroutines. For more information on how to do this, see the section dealing with side effects of cancellations below.

In the underlying implementation, the child coroutine notifies its parent of the cancellation by throwing an exception. The parent coroutine uses the cancellation reason passed in to decide whether to handle the exception. If a child coroutine is cancelled because of a CancellationException, no additional operations are required for its parent.

A new coroutine cannot be started again in a canceled scope

If you’re using the AndroidX KTX library, you don’t need to create your own scopes in most cases, so you don’t have to be responsible for canceling them. Use viewModelScope if you are operating in a ViewModel scope, or lifecycleScope if you are starting a coroutine in a life-cycle related scope. ViewModelScope and lifecycleScope are both CoroutineScope objects, and they are both cancelled at the appropriate point in time. For example, when the ViewModel is cleared, coroutines started in its scope are also cancelled.

Why didn’t the coroutine process stop?

If we simply call the cancel method, it does not mean that the task being processed by the coroutine will stop. If you use coroutines to do some relatively heavy work, such as reading multiple files, your code will not automatically stop doing so.

Let’s take a simpler example and see what happens. Suppose we need to print “Hello” twice per second using a coroutine. Let’s let the coroutine run for a second and then cancel it. One version of the implementation looks like this:

Let’s take a step-by-step look at what’s going on. When the launch method is called, we create an active state coroutine. We then run the coroutine for 1,000 milliseconds and print the following:

Hello 0
Hello 1
Hello 2
Copy the code

When the job.cancel method is called, our coroutine transitions to the cancelling state. But then we see Hello 3 and Hello 4 printed on the command line. When the coroutine finishes processing the task, it transitions to the cancelled state.

The tasks handled by the coroutine don’t stop just when the Cancel method is called; instead, we need to modify the code to periodically check if the coroutine is active.

Allows your coroutine to be cancelled

You need to make sure that all code implementations that use coroutine processing tasks are cooperative, that is, they are handled with coroutine cancellation, so you can check periodically during task processing to see if coroutines have been canceled, or if the current coroutine has been canceled before processing time-consuming tasks. For example, if you get multiple files from disk, check to see if the coroutine is canceled before you start reading the contents of the files. With processing like this, you can avoid dealing with unnecessary CPU-intensive tasks.

val job = launch {
    for(file in files) {
        // TODO checks if coroutines are cancelled
        readFile(file)
    }
}
Copy the code

All pending functions (withContext, delay, etc.) in Kotlinx. coroutines are cancelable. If you use any of these functions, there is no need to check to see if the coroutine has been canceled, stop the task execution, or throw a CancellationException. However, if you do not use these functions, in order to make your code work with coroutine cancellation, you can use the following two methods:

  • Check job. IsActive or use ensureActive()
  • Use yield() to allow other tasks to proceed

Check the job active status

Let’s look at the first method, which adds a check for coroutine state to our while(I <5) loop:

Job. IsActive while (I < 5&&isactive)Copy the code

This means that our task will only be executed when the coroutine is active. This also means that we can check if we want to handle other actions outside of the while loop, such as logging out after the job is canceled! IsActive then proceed with the corresponding processing.

Another useful method, ensureActive(), is provided in Coroutine’s code base, and is implemented as follows:

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

This method immediately throws an exception if the job is inactive, and we can use this method at the beginning of the while loop.

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

By using the ensureActive approach, you eliminate the need for if statements to check isActive status, which reduces the amount of boilerplate code but also reduces the flexibility to handle actions like log printing.

Use the yield() function to run other tasks

Use yield() if the task being processed is 1) CPU-intensive, 2) likely to exhaust thread pool resources, or 3) needs to be allowed to work on other tasks without adding more threads to the thread pool. If the job is completed, the first task handled by yield will be to check the completion status of the job, and then exit the coroutine by throwing a CancellationException. Yield can be used to periodically check the first function called, such as the ensureActive() method mentioned above.

Job. Join 🆚 Deferred. Await cancellation * *

There are two ways to wait for coroutine results: a job from launch can call the Join method, and a Deferred (one of the job types) returned by Async can call the await method.

Job.join suspends the coroutine until the task is finished. When used with job.cancel, the following is done:

  • If you call job.Cancel followed by job.Join, the coroutine will remain suspended until the task processing completes;
  • Calling job.cancel after job.join makes no difference because the job is already done.

If you care about coroutine processing results, you should use Deferred. When the coroutine completes, the result is returned as Deferred. Await. Deferred is one type of Job that can also be cancelled.

Calling await on a cancelled deferred raises JobCancellationException.

valDeferred = async {... } deferred.cancel()val result = deferred.await() // Throws JobCancellationException
Copy the code

Why do I get this exception? The role of await is to keep the coroutine pending until the result of coroutine processing comes out, because if the coroutine is cancelled then the coroutine will not continue to evaluate and no result will be produced. Therefore, calling await after coroutine cancellation raises JobCancellationException: because the Job has been canceled.

On the other hand, nothing happens if you call deferred.await after deferred.cancel, because the coroutine is finished processing.

Handles side effects of coroutine cancellation

Suppose you want to perform a specific action after coroutine cancellation, such as closing a resource that might be in use, or logging for cancellation needs, or performing some other cleanup code. We can do this in several ways:

Check! isActive

If you perform isActive checks periodically, you can clean up resources once you break out of the WHILE loop. Previous code can be updated to the following version:

While (I < 5 && isActive) {if (...) Println (" Hello ${i++} ") nextPrintTime += 500L}} )Copy the code

You can view the full version.

So now, when the coroutine is no longer active, it exits the while loop and can do some cleanup.

Try catch finally

Because a CancellationException is thrown when a coroutine is canceled, we can place the pending task ina try/catch block and then perform any cleanup tasks that need to be done ina 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

However, once the cleanup we need to perform is also suspended, the code above can no longer work, because once the coroutine is in the canceled state, it can no longer go to the suspend state. You can view the full code.

Coroutines in the canceled state cannot be suspended

When the suspension function is called after the coroutine has been canceled, we need to place the code for the cleanup task in the NonCancellable CoroutineContext. This suspends running code and holds the canceled state of the coroutine until task processing is complete.

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

You can see how it works.

SuspendCancellableCoroutine and invokeOnCancellation

If you pass suspendCoroutine method will be back to back for coroutines, then you should use suspendCancellableCoroutine method. You can use the continuation. InvokeOnCancellation to perform cancel operations:

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

In order to enjoy the benefits of structured concurrency, and to ensure that we are not doing anything unnecessary, we need to ensure that the code is cancelable.

Use CoroutineScopes defined in Jetpack: viewModelScope or lifecycleScope, which cancel the tasks they handle when the scope completes. If you are creating your own CoroutineScope, be sure to bind it to the job and call Cancel if needed.

The cancellation of coroutine code needs to be collaborative, so update the code to check for the cancellation of coroutines in a deferred manner and avoid unnecessary operations.

Now that you know some of the basic concepts of coroutines in part 1 of this series, and cancellation of coroutines in Part 2, we will continue to delve into exception handling in part 3 of this series. Please stay tuned for updates.