Coroutine Cancellation 101

栏目: IT技术 · 发布时间: 4年前

内容简介:Wanna keep in touch and be notified of similar posts?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 exp
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.


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

趣学算法

趣学算法

陈小玉 / 人民邮电出版社 / 2017-7-1 / 89.00元

本书内容按照算法策略分为7章。 第1章从算法之美、简单小问题、趣味故事引入算法概念、时间复杂度、空间复杂度的概念和计算方法,以及算法设计的爆炸性增量问题,使读者体验算法的奥妙。 第2~7章介绍经典算法的设计策略、实战演练、算法分析及优化拓展,分别讲解贪心算法、分治算法、动态规划、回溯法、分支限界法、线性规划和网络流。每一种算法都有4~10个实例,共50个大型实例,包括经典的构造实例和实......一起来看看 《趣学算法》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具