15

Coroutine Cancellation 101

 4 years ago
source link: https://zsmb.co/coroutine-cancellation-101/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
This article assumes that you already know the basics of coroutines, and what a suspending function is.

Starting a coroutine

The rule of thumb with coroutines is that suspending functions can only be called from suspending functions. But how do we make our first call to a suspending function, if we need to already be in a suspending function to do so? This is the purpose that coroutine builders serve. They let us bridge the gap between the regular, synchronous world and the suspenseful world of coroutines.

launch is the usually the first coroutine builder that we at when learning coroutines. The “trick” with launch is that it’s a non-suspending function, but it takes a lambda parameter, which is a suspending function:

fun launch(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend () -> Unit
): Job

It creates a new coroutine, which will execute the suspending block of code passed to it. launch is a fire-and-forget style coroutine builder, as it’s not supposed to return a result. So it returns immediately after starting the coroutine, while the started coroutine fires of asynchronously.

It does return a Job instance, which, according to the documentation…

is a cancellable thing with a life-cycle that culminates in its completion.

Basically, this Job represents a piece of work being performed for us by a coroutine, and can be used to keep track of that coroutine. We can check if it’s still running, or cancel it:

val job = GlobalScope.launch {
    println("Job is running...")
    delay(500)
    println("Job is done!")
}

Thread.sleep(200L)
if (job.isActive) {
    job.cancel()
}

delay is a handy suspending function that we can use inside coroutines to wait for a given amount of the time in a non-blocking way.

Since the coroutine above is cancelled before the delay is over, only its first print statement will be executed.

Job is running...

This happens because we call cancel while the suspending delay call is happening in the coroutine.

Blocking execution

What if there were no suspension points in the coroutine, and its entire body was just blocking code? For example, if we replace the delay call with Thread.sleep :

val job = GlobalScope.launch {
    println("Job is running...")
    Thread.sleep(500L)
    println("Job is done!")
}

Thread.sleep(200L)
if (job.isActive) {
    job.cancel()
}

If we run the code again, we’ll see this output:

Job is running...
Job is done!

We’re in trouble, cancellation is now broken! It turns out that coroutines can only be cancelled cooperatively . While a coroutine is running continuously blocking code, it won’t be notified of being cancelled.

Cooperation

Why doesn’t the Thread that the coroutine is running on get interrupted forcibly? Because doing something like this would be dangerous. Whenever you write blocking code, you expect all those lines of code to be executed together, one after another. (Kind of like in a transaction!) If this gets cuts off in the middle, completely unexpected things can happen in the application. Hence the cooperative approach instead.

So how do we cooperate? For one, we can call functions from kotlinx.coroutines that support cancellation already - delay was an example of this. If our coroutine is cancelled while we are waiting for delay , it will throw a JobCancellationException instead of returning normally. If our coroutine was cancelled some time before a call delay , and this cancellation wasn’t handled, delay will also throw this exception as soon as it’s called.

For example, let’s say that we have a list of entities to save to two different places which we perform by calling these two blocking functions:

fun saveToServer(entity: String)
fun saveToDisk(entity: String)

We don’t want to end up in a situation where we’ve saved an entity to one of these places, but not the other. We either want both of these calls to run for an entity, or neither of them.

A first approach to this problem would be to use withContext , to suspend the caller, and move this operation to another thread. The code below will block a thread on the IO dispatcher for the entire length of our operation, which ensures that this coroutine is practically never cancelled:

suspend fun processEntities(entities: List<String>) = withContext(Dispatchers.IO) {
    entities.forEach { entity ->
        saveToServer(entity)
        saveToDisk(entity)
    }
}

However, we can also add cancellation support, by checking if our current coroutine has been cancelled, manually. For example, we can do this after processing each entity:

suspend fun processEntities(entities: List<String>) = withContext(Dispatchers.IO) {
    entities.forEach { entity ->
        saveToDisk(entity)
        saveToServer(entity)
        if (!isActive) {
            return@withContext
        }
    }
}

If our coroutine is cancelled while we run the blocking part of our code, that entire blocking part will still be executed together, but then we’ll eventually notice the cancellation at the end of the loop, and stop performing further work, in a safe way.

Yielding

Another handy function we have is yield , which has the original purpose of performing a manual suspension of the current coroutine, just to give other coroutines waiting for the same dispatcher a chance to execute. It essentially reschedules the rest of our coroutine to be executed on the same dispatcher that it’s currently on. If there’s nothing else waiting to use this dispatcher, this is essentially just a 0-length delay.

However, yield also handles cancellation (meaning that it also throws a JobCancellationException when it’s invoked in a cancelled coroutine), so we can call it every once in a while when performing lots of blocking work, to provide opportunity for the coroutine to be cancelled. This is done completely manually though, explicitly, which means we are aware of the possibility of cancellation.

yield can easily replace manual cancellation checks, if terminating with an exception upon cancellation is good enough for us:

suspend fun processEntities(entities: List<String>) = withContext(Dispatchers.IO) {
    entities.forEach { entity ->
        saveToDisk(entity)
        saveToServer(entity)
        yield()
    }
}

Just like with delay , even if the coroutine happens to have been cancelled some time before yield , it will notice this, and throw an exception. The cancellation doesn’t have to happen at the exact time that yield is called.

Note that if there’s some cleanup of the coroutine to do (freeing up resources, etc.) when cancelled, manual cancellation checks can still be very handy, and should be used instead of yield .

Conclusion

We’ve seen that coroutines always rely on cooperative cancellation. We can either check if the coroutine we’re executing has been cancelled ourselves, or if we invoke any kotlinx.coroutines functions in our code, these will perform the check for us, and attempt to stop the execution of our coroutine with an exception.

If you want to learn even more about coroutine cancellation, the following talk is for you: KotlinConf 2019: Coroutines! Gotta catch ‘em all! by Florina Muntenescu & Manuel Vivo .

Feedback on this article is very welcome. One of the best places to leave it would be on reddit .

Wanna keep in touch and be notified of similar posts? Follow me @zsmb13 on Twitter !

Continue reading...

Wrap-up 2019

Another year has come to an end, so it's time to reflect again. I'll attempt to sum up what I've done this year, how that compares to what I was planning to do at this same time last year, and what I expect to be next.

Let's Review: Pokedex

In what may be the start of a new series, I code review a project that was posted on reddit recently and got very popular very quickly. Let's see what we can learn from it?

Primaries Matter (a discussion of constructors)

Primary constructors play a fundamental role in Kotlin classes. Let's take a close look at them, and really understand what exactly is part of a primary constructor, and what makes this constructor so special.

Retrofit meets coroutines

Retrofit's coroutine support has been a long time coming, and it's finally coming to completion. Take a look at how you can use it to neatly integrate networking into an application built with coroutines.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK