协程中的取消

重点 (Top highlight)

In development, as in life, we know it’s important to avoid doing more work than needed as it can waste memory and energy. This principle applies to coroutines as well. You need to make sure that you control the life of the coroutine and cancel it when it’s no longer needed — this is what structured concurrency represents. Read on to find out the ins and outs of coroutine cancellation.

在开发中和生活中,我们都知道避免做过多的工作很重要,因为这会浪费内存和精力。 这个原则也适用于协程。 您需要确保控制协程的生命,并在不再需要协程时取消它–这就是结构化并发表示的。 继续阅读以了解协程取消的来龙去脉。

If you prefer to see a video on this check out the talk Manuel Vivo and I gave at KotlinConf’19 on coroutines cancellation and exceptions:

如果您希望观看此视频,请查看我和Manuel Vivo在KotlinConf'19上发表的有关协程取消和异常的演讲:

⚠️ In order to follow the rest of the article without any problems, reading and understanding Part I of the series is required.

为了使本文的其余部分没有任何问题,请阅读并理解本系列的第一部分。

取消通话 (Calling cancel)

When launching multiple coroutines, it can be a pain to keep track of them or cancel each individually. Rather, we can rely on cancelling the entire scope coroutines are launched into as this will cancel all of the child coroutines created:

当启动多个协程时,跟踪它们或单独取消它们可能很痛苦。 相反,我们可以依靠取消启动整个范围的协程,因为这将取消创建的所有子协程:

// assume we have a scope defined for this layer of the appval job1 = scope.launch { … }
val job2 = scope.launch { … }scope.cancel()

Cancelling the scope cancels its children

取消范围将取消其子级

Sometimes you might need to cancel only one coroutine, maybe as a reaction to a user input. Calling job1.cancel ensures that only that specific coroutine gets cancelled and all the other siblings are not affected:

有时,您可能只需要取消一个协程,这可能是对用户输入的一种React。 调用job1.cancel可确保仅取消特定协程,并且不影响所有其他同级兄弟:

// assume we have a scope defined for this layer of the appval job1 = scope.launch { … }
val job2 = scope.launch { … }// First coroutine will be cancelled and the other one won’t be affectedjob1.cancel()

A cancelled child doesn’t affect other siblings

被取消的孩子不会影响其他兄弟姐妹

Coroutines handle cancellation by throwing a special exception: CancellationException. If you want to provide more details on the cancellation reason you can provide an instance of CancellationException when calling .cancel as this is the full method signature:

协程通过引发特殊异常来处理取消: CancellationException 。 如果要提供有关取消原因的更多详细信息,可以在调用.cancel时提供CancellationException的实例,因为这是完整的方法签名:

fun cancel(cause: CancellationException? = null)

If you don’t provide your own CancellationException instance, a default CancellationException will be created (full code here):

如果您不提供自己的CancellationException实例,则将创建一个默认的CancellationException ( 此处的完整代码):

public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}

Because CancellationException is thrown, then you will be able to use this mechanism to handle the coroutine cancellation. More about how to do this in the Handling cancellation side effects section below.

因为CancellationException被抛出,所以您将能够使用此机制来处理协程取消。 有关如何执行此操作的更多信息,请参见下面的“ 处理取消副作用”部分。

Under the hood, the child job notifies its parent about the cancellation via the exception. The parent uses the cause of the cancellation to determine whether it needs to handle the exception. If the child was cancelled due to CancellationException, then no other action is required for the parent.

在后台,子作业通过异常通知其父项有关取消的信息。 父级使用取消的原因来确定是否需要处理异常。 如果由于CancellationException了子级,则父级不需要其他操作。

⚠️Once you cancel a scope, you won’t be able to launch new coroutines in the cancelled scope.

⚠️一旦取消合并范围,将无法在已取消合并范围内启动新的协程。

If you’re using the androidx KTX libraries in most cases you don’t create your own scopes and therefore you’re not responsible for cancelling them. If you’re working in the scope of a ViewModel, using viewModelScope or, if you want to launch coroutines tied to a lifecycle scope, you would use the lifecycleScope. Both viewModelScope and lifecycleScope are CoroutineScope objects that get cancelled at the right time. For example, when the ViewModel is cleared, it cancels the coroutines launched in its scope.

如果您在大多数情况下使用androidx KTX库,则不会创建自己的作用域,因此不负责取消作用域。 如果你在一个范围工作ViewModel ,使用viewModelScope或者,如果你想启动协同程序绑在生命周期的范围,你可以使用lifecycleScopeviewModelScopelifecycleScope都是CoroutineScope对象,可以在适当的时间取消它们。 例如, 当清除ViewModel时 ,它将取消在其范围内启动的协程。

为什么我的协程工作没有停止? (Why isn’t my coroutine work stopping?)

If we just call cancel, it doesn’t mean that the coroutine work will just stop. If you’re performing some relatively heavy computation, like reading from multiple files, there’s nothing that automatically stops your code from running.

如果我们仅调用cancel ,并不意味着协程工作将停止。 如果您要执行一些相对繁重的计算,例如从多个文件中读取,那么没有什么会自动阻止您的代码运行。

Let’s take a more simple example and see what happens. Let’s say that we need to print “Hello” twice a second using coroutines. We’re going to let the coroutine run for a second and then cancel it. One version of the implementation can look like this:

让我们举一个更简单的例子,看看会发生什么。 假设我们需要使用协程每秒两次打印“ Hello”。 我们将让协程运行一秒钟,然后将其取消。 该实现的一个版本如下所示:

Let’s see what happens step by step. When calling launch, we’re creating a new coroutine in the active state. We’re letting the coroutine run for 1000ms. So now we see printed:

让我们一步一步地看看会发生什么。 调用launch ,我们将在活动状态下创建一个新的协程。 我们让协程运行1000毫秒。 所以现在我们看到了:

Hello 0
Hello 1
Hello 2

Once job.cancel is called, our coroutine moves to Cancelling state. But then, we see that Hello 3 and Hello 4 are printed to the terminal. Only after the work is done, the coroutine moves to Cancelled state.

调用job.cancel ,协程将进入取消状态。 但是然后,我们看到Hello 3和Hello 4被打印到终端上。 只有在完成工作后,协程才会进入已取消状态。

The coroutine work doesn’t just stop when cancel is called. Rather, we need to modify our code and check if the coroutine is active periodically.

协程工作不仅会在调用cancel时停止。 相反,我们需要修改代码并检查协程是否定期处于活动状态。

Cancellation of coroutine code needs to be cooperative!

协程代码的取消需要配合!

使协程工作可取消 (Making your coroutine work cancellable)

You need to make sure that all the coroutine work you’re implementing is cooperative with cancellation, therefore you need to check for cancellation periodically or before beginning any long running work. For example, if you’re reading multiple files from disk, before you start reading each file, check whether the coroutine was cancelled or not. Like this you avoid doing CPU intensive work when it’s not needed anymore.

您需要确保正在执行的所有协程工作与取消协作,因此您需要定期检查取消或在开始任何长期运行的工作之前进行检查。 例如,如果要从磁盘读取多个文件,则在开始读取每个文件之前,请检查协程是否已取消。 这样,您可以避免在不再需要CPU时进行大量工作。

val job = launch {
for(file in files) {
// TODO check for cancellation
readFile(file)
}
}

All suspend functions from kotlinx.coroutines are cancellable: withContext, delay etc. So if you’re using any of them you don’t need to check for cancellation and stop execution or throw a CancellationException. But, if you’re not using them, to make your coroutine code cooperative we have two options:

来自kotlinx.coroutines所有暂停函数都是可以取消的: withContextdelay等。因此,如果您使用它们中的任何一个,则无需检查取消和停止执行或抛出CancellationException 。 但是,如果您不使用它们,为了使协程代码协作,我们有两种选择:

  • Checking job.isActive or ensureActive()

    检查job.isActiveensureActive()

  • Let other work happen using yield()

    使用yield()让其他工作发生

检查作业的活动状态 (Checking for job’s active state)

One option is in our while(i<5) to add another check for the coroutine state:

我们的while(i<5)有一个选项可以添加针对协程状态的另一种检查:

// Since we're in the launch block, we have access to job.isActive
while (i < 5 && isActive)

This means that our work should only be executed while the coroutine is active. This also means that once we’re out of the while, if we want to do some other action, like logging if the job was cancelled, we can add a check for !isActive and do our action there.

这意味着我们的工作只能在协同程序处于活动状态时执行。 这也意味着,一旦我们没有时间,如果要执行其他操作,例如记录作业是否被取消,则可以添加!isActive的检查并在那里执行操作。

The Coroutines library provides another helpful method - ensureActive(). Its implementation is:

Coroutines库提供了另一个有用的方法ensureActive() 。 它的实现是:

fun Job.ensureActive(): Unit {
if (!isActive) {
throw getCancellationException()
}
}

Because this method instantaneously throws if the job is not active, we can make this the first thing we do in our while loop:

因为如果作业不活跃,此方法会立即抛出,因此我们可以将其作为while循环中要做的第一件事:

while (i < 5) {
ensureActive()

}

By using ensureActive, you avoid implementing the if statement required by isActive yourself, decreasing the amount of boilerplate code you need to write, but lose the flexibility to perform any other action like logging.

通过使用ensureActive ,可以避免自己实现isActive所需的if语句,从而减少了您需要编写的样板代码量,但是却失去了执行其他任何操作(如记录日志)的灵活性。

使用yield()让其他工作发生 (Let other work happen using yield())

If the work you’re doing is 1) CPU heavy, 2) may exhaust the thread pool and 3) you want to allow the thread to do other work without having to add more threads to the pool, then use yield(). The first operation done by yield will be checking for completion and exit the coroutine by throwing CancellationException if the job is already completed. yield can be the first function called in the periodic check, like ensureActive() mentioned above.

如果您正在做的工作是1)CPU繁重,2)可能耗尽线程池,3)您要允许线程执行其他工作而不必向池中添加更多线程,请使用yield() 。 如果作业已经完成,则由yield进行的第一个操作将检查是否完成,并通过引发CancellationException退出协程。 yield可以是定期检查中调用的第一个函数,例如上述的ensureActive()

Job.join vs Deferred.await取消 (Job.join vs Deferred.await cancellation)

There are two ways to wait for a result from a coroutine: jobs returned from launch can call join and Deferred (a type of Job) returned from async can be await’d.

等待协程结果的方法有两种: launch返回的作业可以调用joinasync返回的Deferred ( Job )可以await

Job.join suspends a coroutine until the work is completed. Together with job.cancel it behaves as you’d expect:

Job.join暂停协程直到工作完成。 与job.cancel一起使用job.cancel其行为与您期望的一样:

  • If you’re calling job.cancel then job.join, the coroutine will suspend until the job is completed.

    如果您要呼叫job.cancel然后job.join ,协程将暂停,直到作业完成。

  • Calling job.cancel after job.join has no effect, as the job is already completed.

    调用job.canceljob.join有没有影响,因为该作业已经完成。

You use a Deferred when you are interested in the result of the coroutine. This result is returned by Deferred.await when the coroutine is completed. Deferred is a type of Job, and it can also be cancelled.

对协程的结果感兴趣时,可以使用Deferred 。 协程完成后, Deferred.await返回此结果。 DeferredJob ,也可以取消。

Calling await on a deferred that was cancelled throws a JobCancellationException.

在被cancel的延迟调用上调用await会抛出JobCancellationException

val deferred = async { … }deferred.cancel()
val result = deferred.await() // throws JobCancellationException!

Here’s why we get the exception: the role of await is to suspend the coroutine until the result is computed; since the coroutine is cancelled, the result cannot be computed. Therefore, calling await after cancel leads to JobCancellationException: Job was cancelled.

这就是我们得到异常的原因: await的作用是暂停协程,直到计算出结果为止; 由于协程被取消,因此无法计算结果。 因此,在cancel之后调用await会导致JobCancellationException: Job was cancelled

On the other hand, if you’re calling deferred.cancel after deferred.await nothing happens, as the coroutine is already completed.

在另一方面,如果你打电话deferred.canceldeferred.await什么也没有发生,作为协同程序已经完成。

处理取消的副作用 (Handling cancellation side effects)

Let’s say that you want to execute a specific action when a coroutine is cancelled: closing any resources you might be using, logging the cancellation or some other cleanup code you want to execute. There are several ways we can do this:

假设您要在取消协程时执行特定操作:关闭您可能正在使用的任何资源,记录取消或要执行的其他清理代码。 我们有几种方法可以做到这一点:

检查!isActive (Check for !isActive)

If you’re periodically checking for isActive, then once you’re out of the while loop, you can clean up the resources. Our code above could be updated to:

如果您要定期检查isActive ,那么一旦退出while循环,就可以清理资源。 我们上面的代码可以更新为:

while (i < 5 && isActive) {
// print a message twice a second
if (…) {
println(“Hello ${i++}”)
nextPrintTime += 500L
}
}// the coroutine work is completed so we can cleanup
println(“Clean up!”)

See it in action here.

看到它在这里行动。

So now, when the coroutine is no longer active, the while will break and we can do our cleanup.

因此,现在,当协程不再活动时, while会断开,我们可以进行清理。

最后尝试赶上 (Try catch finally)

Since a CancellationException is thrown when a coroutine is cancelled, then we can wrap our suspending work in try/catch and in the finally block, we can implement our clean up work.

由于CancellationException协程时会引发CancellationException ,因此我们可以将挂起的工作包装在try/catch并在finally块中,我们可以实现清理工作。

val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)} finally {
println(“Clean up!”)
}

}delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!”)

But, if the cleanup work we need to execute is suspending, the code above won’t work anymore, as once the coroutine is in Cancelling state, it can’t suspend anymore. See the full code here.

但是,如果我们需要执行的清理工作正在暂停,则上面的代码将不再起作用,因为协程一旦进入Cancelling状态,便无法再暂停。 在此处查看完整代码。

A coroutine in the cancelling state is not able to suspend!

处于取消状态的协程不能暂停!

To be able to call suspend functions when a coroutine is cancelled, we will need to switch the cleanup work we need to do in a NonCancellable CoroutineContext. This will allow the code to suspend and will keep the coroutine in the Cancelling state until the work is done.

为了能够在协程被取消时调用suspend函数,我们将需要切换在NonCancellable CoroutineContext需要执行的清理工作。 这将使代码暂停,并将协程保持在“ 取消”状态,直到完成工作为止。

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!”)

Check out how this works in practice here.

此处查看如何在实际中工作。

suspendCancellableCoroutine和invokeOnCancellation (suspendCancellableCoroutine and invokeOnCancellation)

If you converted callbacks to coroutines by using the suspendCoroutine method, then prefer using suspendCancellableCoroutine instead. The work to be done on cancellation can be implemented using continuation.invokeOnCancellation:

如果您通过使用suspendCoroutine方法将回调转换为协程,则建议改用suspendCancellableCoroutine 。 可以使用continuation.invokeOnCancellation来实现要取消的工作:

suspend fun work() {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
// do cleanup
}
// rest of the implementation
}

To realise the benefits of structured concurrency and ensure that we’re not doing unnecessary work you need to make sure that you’re also making your code cancellable.

为了实现结构化并发的好处并确保我们没有做不必要的工作,您需要确保同时使代码可取消。

Use the CoroutineScopes defined in Jetpack: viewModelScope or lifecycleScope that cancels their work when their scope completes. If you’re creating your own CoroutineScope, make sure you’re tying it to a job and calling cancel when needed.

使用CoroutineScopes在Jetpack的定义: viewModelScopelifecycleScope抵消时,他们的工作及其范围完成。 如果要创建自己的CoroutineScope ,请确保将其与作业绑定,并在需要时调用cancel。

The cancellation of coroutine code needs to be cooperative so make sure you update your code to check for cancellation to be lazy and avoid doing more work than necessary.

协程代码的取消需要配合进行,因此请确保更新代码以检查取消是懒惰的,并避免执行不必要的工作。

Find out more about patterns for work that shouldn’t be cancelled from this post:

在这篇文章中找到有关不应取消的工作模式的更多信息:

翻译自: https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值