This is the second article in a series on coroutine cancellations and exceptions, and it is highly recommended!

Originally written by Florina Muntenescu

There was a Cancellation in coroutines

Translator: Bingxin said

In software development and in life, we should avoid too much useless work, which only wastes memory and energy. The same principle applies to coroutines. Making sure you can control the life cycle of the coroutine and cancel it when it doesn’t need to work is called structured concurrency. Read on to learn the ins and outs of coroutine cancellation.

If you prefer video, check out the link below to see Manuel Vivo and me talk at KotlinConf ’19.

To help you understand the rest of this article, I recommend reading the First article in the series, Coroutines: First Things First

Call to cancel

When starting multiple coroutines, it is painful to trace and cancel them one by one. Instead, we can rely on canceling the entire coroutine scope to cancel all child coroutines created through it.

// Suppose we define a coroutine scope here

valJob1 = scope.launch {... }

valJob2 = scope.launch {... }

scope.cancel()

Copy the code

Removing a coroutine scope cancels all of its children.

Sometimes you may simply cancel a coroutine, for example in response to user input. Job1. cancel ensures that only certain coroutines are cancelled, while others are unaffected.

// Suppose we define a coroutine scope here

valJob1 = scope.launch {... }

valJob2 = scope.launch {... }

// The first coroutine will be cancelled, while the others are unaffected

job1.cancel()

Copy the code

Canceling subcoroutines does not affect other subcoroutines.

Coroutines cancel Coroutines by throwing a special CancellationException. If you want to provide more details about the cancellation reason, you can pass in a custom CancellationException instance by calling cancel() : CancellationException

fun cancel(cause: CancellationException? = null)

Copy the code

If you do not provide your own CancellationException instance, the default implementation will be provided. (Full code here)

public override fun cancel(cause: CancellationException?). {

cancelInternal(cause ? : defaultCancellationException())

}

Copy the code

Since a CancellationException was thrown, you can use this mechanism to handle cancellations of coroutines. See the section dealing with side effects of coroutine cancellation below.

In effect, the child Job notifies the father of its cancellation through an exception mechanism. The father determines whether to handle the exception by the reason of the cancellation. If the subtask is cancelled due to a CancellationException, the father does no additional processing.

⚠️ Once the coroutine scope is cancelled, new coroutines cannot be created in it.

If you’re using the AndroidX KTX library, you don’t need to create your own scopes in most cases, so you’re not responsible for canceling them. If you’re working in ViewModel, use viewModelScope directly. If you want to start coroutines in life-cycle dependent roles, use lifecyclescope directly.

ViewModelScope and lifecycleScope are both CoroutineScope objects and are automatically cancelled when appropriate. For example, when the ViewModel enters the Cleared state, all coroutines started in it are automatically cancelled.

Why doesn’t work stop in the coroutine?

When we call Cancel, it does not mean that work in the coroutine stops immediately. If you are doing something heavy, such as reading multiple files, removing coroutines does not automatically stop your code from running.

Let’s do a little test to see what happens. Print “Hello” twice per second through the coroutine, run for 1 second and then cancel the coroutine. The implementation code is as follows:

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

Let’s look at what happens step by step. When launch is called, a new coroutine is created and in the active state. Then run the coroutine for 1000ms and print the following:

Hello 0
Hello 1
Hello 2
Copy the code

Once job.cancel () is called, the coroutine becomes Cancelling. However, the console still prints Hello3 and Hello4. The concordance goes into Cancelled only when the work is finished.

Work in the coroutine does not stop immediately when cancel is called. Therefore, we need to modify the code to periodically check if the coroutine is active.

Code needs to cooperate to complete the cancellation of coroutine!

Let your coroutine work be cancelled

You need to make sure that any coroutines you create are implementation-cancable, so you need to check the coroutine status periodically or before performing time-consuming tasks. For example, if you are reading multiple files from disk, check to see if the coroutine is canceled before reading each file. This avoids unnecessary CPU intensive work.

val job = launch {

    for(file in files) {

        // TODO check for cancellation

        readFile(file)

    }

}

Copy the code

All pending functions in Kotlinx. coroutines are cancelable: withContext, delay, and so on. So you don’t need to check when you use them. Instead, in order for your coroutine code to work with cancellation, there are two options:

  • checkjob.isActiveorensureActive
  • Let other work happen using yield()

Checking Job Status

One option is to add code to check the state of the coroutine in while(I <5).

// Since we're in the launch block, we have access to job.isActive

while (i < 5 && isActive)

Copy the code

This means that our work will only be performed when the coroutine is active. If we want to do something else after the coroutine has been cancelled, such as printing logs, we can check! IsActive.

The coroutine library provides a useful function, ensureActive(), which is implemented like this:

fun Job.ensureActive(a)Unit {

    if(! isActive) {

         throw getCancellationException()

    }

}

Copy the code

EnsureActive () raises an exception when the coroutine is not active, so you can also do this:

while (i < 5) {

    ensureActive()

    …

}

Copy the code

Using ensureActive() eliminates the need for you to manually check isActive, reducing boilerplate code but losing flexibility, such as printing logs after coroutines are cancelled.

Use the yield ()

If the task in progress looks like this:

  1. It consumes a lot of CPU resources
  2. Thread pool resources may be exhausted
  3. Allows you to perform other tasks without adding threads to the thread pool

Use yield() at this point. The first thing yield does is check to see if the task is complete, and if the Job is, it throws a CancellationException to end the coroutine. Yield should be invoked first in a scheduled check, just like ensureActive mentioned earlier.

Cancellation of job.join () and deferred.await ()

There are two ways to get the return value of a coroutine. The first is a Job that is launched by the launch method and can call its join() method. The async method initiates the Deferred(which is also a Job) and can call its await() method.

Job.join suspends the coroutine until the task ends. This, combined with job.cancel (), behaves like this:

  • If I calljob.cancelCall again,job.join, the coroutine will still be suspended until the task ends.
  • injob.joinAfter the calljob.cancelThere is no mission impact because the mission is over.

The result of coroutine execution can also be obtained through Deferred. When the task ends, Deferred. Await returns the execution result. Deferred is a Job that can also be cancelled.

A JobCancellationException is thrown for a deferred invoked await method that has been cancelled.

valDeferred = async {... }



deferred.cancel()

val result = deferred.await() // throws JobCancellationException!

Copy the code

The purpose of await is to suspend the coroutine until the result is computed. The result cannot be computed because the coroutine was cancelled. So, cancel then await could lead to JobCancellationException: Job was cancelled.

Also, if you call deferred.cancel after deferred.await, nothing will happen because the task is finished.

Handle side effects of coroutine cancellation

Now suppose we need to do some specific tasks when coroutine cancellation occurs: close the resource in use, print the cancellation log, or some other cleanup class code that you want to perform. There are several ways to do this.

Check! isActive

Periodically check isActive, and once you break out of the WHILE loop, you can clean up resources. Our sample code is updated as follows:

while (i < 5 && isActive) {

    // print a message twice a second

    if(...). {

Println (" Hello ${i++} ")

        nextPrintTime += 500L

    }

}

// The coroutine has finished its work

Println (" the Clean up!" )

Copy the code

Try catch finally

Since a coroutine will throw a CancellationException when cancelled, we can wrap the hang function ina try/catch block so that we can do resource cleaning in the finally block.

val job = launch {

   try {

      work()

   } catch (e: CancellationException){

Println (" Work cancelled!" )

    } finally {

Println (" the Clean up!" )

    }

}

delay(1000L)

Println (" Cancel!" )

job.cancel()

Println (" Done!" )

Copy the code

However, if the function that performs the cleanup task also needs to be suspended, the above code is invalid because the coroutine is already Cancelling. The full code is here.

Cancelled coroutines can no longer be suspended!

To be able to call the suspend function when the coroutine is cancelled, we need to switch the task to the NonCancellable coroutine context for execution, which will hold the coroutine in Cancelling state until the task ends.

val job = launch {

   try {

      work()

   } catch (e: CancellationException){

Println (" Work cancelled!" )

    } finally {

      withContext(NonCancellable){

         delay(1000L// or some other suspend fun

Println (" Cleanup done!" )

      }

    }

}

delay(1000L)

Println (" Cancel!" )

job.cancel()

Println (" Done!" )

Copy the code

Here you can practice.

SuspendCancellableCoroutine and invokeOnCancellation

If you use suspendCoroutine to transform the callback for coroutines, so please consider using suspendCancellableCoroutine. Coroutines cancel when the need for work can be a continuation. InvokeOnCancellation in implementation.

suspend fun work(a) {

   return suspendCancellableCoroutine { continuation ->

       continuation.invokeOnCancellation { 

          // do cleanup

       }

   // rest of the implementation

}

Copy the code

The last

To achieve structured concurrency and avoid useless work, you must ensure that your tasks can be cancelled.

Use the coroutine scopes defined in Jetpack (viewModelScope and lifecycleScope) to help you automatically cancel tasks. If you use your own coroutine scope, bind the Job and cancel it when appropriate.

Cancellation of coroutines requires code implementation, so make sure you detect cancellation in your code to avoid extra useless work.

But, in some work modes, tasks should not be cancelled? So, wait for the fourth article in this series on how to do this.


That’s all for today’s article. There are two more articles in this series, both of which are wonderful. Scan the qr code below and keep paying attention!


This article is formatted using MDNICE