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.


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

查看所有标签

猜你喜欢:

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

解密硅谷

解密硅谷

[美]米歇尔 E. 梅西纳(Michelle E. Messina)、乔纳森 C. 贝尔(Jonathan C. Baer) / 李俊、李雪 / 机械工业出版社 / 2018-12 / 50.00

《解密硅谷》由身处硅谷最中心的连续创业者米歇尔·梅西纳和资深的投资人乔纳森·贝尔联合撰写,二人如庖丁解牛一般为读者深入剖析硅谷成功的原因:从硅谷的创新机制、创业生态、投资领域的潜规则、秘而不宣的价值观等角度,让阅读本书的人能够在最短的时间内,拥有像硅谷人一样的商业头脑,从而快速发现机遇,顺利地躲过创业的坑,熬过创业生死挑战中的劫数,带领初创公司顺利地活下去,并实现快速增长。 如果初创公司能够......一起来看看 《解密硅谷》 这本书的介绍吧!

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具