在日常开发中,我们都知道应该避免不必要的任务处理,以节省设备内存空间和功耗——这个原则同样适用于协程。需要控制协程的生命周期,不需要的时候取消。这也是结构化并发所提倡的。继续阅读本文以了解协程取消的来龙去脉。为了更好地理解本文的内容,建议您先阅读本系列的第一篇文章:协程中的取消和异常|核心概念介绍。在启动多个协程时调用cancel方法是一件很头疼的事情,无论是跟踪协程的状态,还是单独取消每个协程。但是,我们可以通过直接取消协程启动中涉及的整个范围来解决这个问题,因为这会取消所有已创建的子协程。//假设我们已经定义了一个作用域valjob1=scope.launch{…}valjob2=scope.launch{…}scope.cancel()1。取消作用域会取消其子协程有时,您可能只需要取消其中一个协程,例如,用户输入一个事件,并响应取消一个正在进行的任务。如下代码所示,调用job1.cancel将保证只有与job1相关的特定协程被取消,而不会影响其他兄弟协程的继续工作。//假设我们定义了一个scopevaljob1=scope.launch{…}valjob2=scope.launch{…}//第一个coroutine会被取消,而另一个不会受到任何影响job1.cancel()2.取消的子协程不会影响其他兄弟协程。协程通过抛出一个特殊的异常CancellationException来处理取消操作。调用.cancel时,您可以传入CancellationException实例以提供有关取消的更多详细信息。该方法的签名如下:funcancel(cause:CancellationException?=null)如果不构造新的CancellationException实例并将其作为参数传入,则会创建一个默认的CancellationException(查看完整代码).publicoverridefuncancel(cause:CancellationException?){cancelInternal(cause?:defaultCancellationException())}完整代码:https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/JobSupport.kt#L612一旦抛出CancellationException,你可以使用这个机制来处理协程的取消。有关如何执行此操作的更多信息,请参阅下面的处理取消的副作用部分。在底层实现中,子协程通过抛出异常通知其父协程取消。父协程通过传入取消原因来决定是否处理异常。如果子协程由于CancellationException而被取消,则不需要对其父协程进行额外的操作。3.不能在取消的范围内再次启动新的协程如果您使用的是androidxKTX库,在大多数情况下您不需要创建自己的范围,因此您不需要负责取消它们。如果您在ViewModel的范围内操作,请使用viewModelScope;如果您在与生命周期相关的范围内启动协程,则使用lifecycleScope。viewModelScope和lifecycleScope都是CoroutineScope对象,它们会在合适的时间点取消。例如,当ViewModel被清除时,在其范围内启动的协程也会随之取消。viewModelScope:https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope生命周期范围:https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#lifecyclescope为什么协程处理的任务没有停止?如果我们只调用cancel方法,并不代表协程处理的任务也会停止。如果你使用协程来处理一些比较繁重的工作,比如读取多个文件,你的代码不会自动停止这个任务。让我们举一个更简单的例子来看看会发生什么。假设我们需要使用协程每秒打印两次“Hello”。我们让协程运行一秒钟,然后取消它。一个版本的实现如下所示:让我们一步一步地看看发生了什么。当启动方法被调用时,我们创建了一个活跃的协程。然后我们让协程运行1000毫秒,打印结果如下:Hello0Hello1Hello2当调用job.cancel方法时,我们的协程变为取消状态。但是后来我们发现命令行打印了Hello3和Hello4。当协程处理的任务结束时,协程变为已取消(cancelled)状态。协程处理的任务并不会在调用cancel方法时就停止,而是需要修改代码,定时检查协程是否活跃。让你的协程可取消你需要确保所有使用协程处理任务的代码实现都是协作的,即它们都处理协程取消,所以你可以在任务处理期间定期检查协程是否已被取消,或者检查当前协程是否在处理耗时任务之前已被取消。例如,如果您从磁盘中获取多个文件,请在开始读取文件内容之前检查协程是否已被取消。通过这样做,您可以避免不必要的CPU密集型任务。valjob=launch{for(fileinfiles){//TODO检查协程是否被取消。readFile(file)}}kotlinx.coroutines中的所有挂起函数(withContext、delay等)都是可取消的。如果您使用这些功能中的任何一个,则无需检查协程是否已被取消,然后停止任务执行,或抛出CancellationException。但是如果不使用这些函数,为了让你的代码配合协程取消,可以使用以下两种方法:检查job.isActive或者使用ensureActive()和yield()让其他任务检查激活状态先看job的第一个方法,在我们的while(i<5)循环中添加一个协程状态的检查://因为在launch的代码块中,所以可以访问job.isActivepropertywhile(i<5&&isActive)就像这样意味着我们的任务只会在协程处于活动状态时执行。同样,这也意味着在while循环之外,如果我们要处理其他行为,比如作业取消后退出,我们可以检查!isActive,然后进行相应的处理。Coroutine的代码库还提供了另一个有用的方法——ensureActive(),实现如下:,我们可以在while循环的开始使用这个方法。while(i<5){ensureActive()...}通过使用ensureActive方法,可以避免使用if语句来检查isActive状态,可以减少样板代码的使用,但是相应地失去了类似日志打印的处理行为的灵活性。如果要处理的任务是1)CPU密集型,2)可能耗尽线程池资源,3)需要让线程处理其他任务而不向线程池添加更多线程,则使用yield()函数运行其他任务任务,然后使用yield()。如果作业已经完成,yield处理的第一个任务将是检查任务的完成状态,然后通过抛出CancellationException直接退出协程。yield可以是周期检查最先调用的函数,比如上面提到的ensureActive()方法。yield():https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.htmlJob.join🆚Deferred.await取消等待协程处理结果有两种方式:launch的job可以调用join方法,async返回的Deferred(job类型之一)可以调用await方法。Job.join暂停协程,直到作业处理完成。当与job.cancel一起使用时,它的行为如下:如果在调用job.cancel之后调用job.join,则协程被挂起,直到任务被处理;在job.join之后调用job。取消无效,因为作业已经完成。Job.join:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html如果你关心协程处理的结果,你应该使用Deferred。当协程完成时,结果将由Deferred.await返回。Deferred是一种也可以取消的Job。延迟:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.htmlDeferred.await:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html在取消的延迟上调用await会抛出JobCancellationException。valdeferred=async{...}deferred.cancel()valresult=deferred.await()//抛出JobCancellationException异常为什么会得到这个异常?await的作用是暂停协程,直到协程处理的结果出来,因为如果取消协程,协程就不会继续执行计算,也就没有结果了。因此,在协程被取消后调用await将抛出JobCancellationException:因为Job已经被取消。另一方面,如果您在deferred.cancel之后调用deferred.await什么也不会发生,因为协程已经完成处理。处理协程取消的副作用假设您想在协程取消后执行特定操作,例如关闭可能正在使用的资源,或记录取消需求,或执行一些剩余的清理代码。有几种方法可以做到这一点:1.检查!isActive如果定期检查isActive,一旦跳出while循环,就可以清理资源。之前的代码可以更新为如下版本:while(i<5&&isActive){if(…){println(“Hello${i++}”)nextPrintTime+=500L}}//协程处理的任务已经完成,所以我们可以做一些清理println("Cleanup!")你可以查看完整版本。完整版:https://pl.kotl.in/loI9DaIYj所以现在,当协程不再活动时,它会退出while循环并可以处理一些清理工作。2.Trycatchfinally由于取消协程时会抛出CancellationException,所以我们可以将待处理的任务放在try/catch代码块中,然后在finally代码块中执行需要完成的清理任务。valjob=launch{try{work()}catch(e:CancellationException){println("Workcancelled!")}finally{println("Cleanup!")}}delay(1000L)println("Cancel!")job.cancel()println("Done!")但是,一旦我们需要进行的清理工作也被挂起,上面的代码就无法继续工作了,因为协程一旦处于取消状态,就无法转为挂起再次(挂起)状态。您可以查看完整代码。完整代码:https://pl.kotl.in/wjPINnWfG处于取消状态的协程不能被挂起当协程被取消需要调用挂起函数时,我们需要将清理任务的代码放在NonCancellable中协程上下文。这将暂停正在运行的代码并使协程保持取消状态,直到任务处理完成。valjob=launch{try{work()}catch(e:CancellationException){println("Workcancelled!")}finally{withContext(NonCancellable){delay(1000L)//或其他一些暂停函数println("Cleanupdone!")}}}delay(1000L)println("Cancel!")job.cancel()println("Done!")你可以看到它是如何工作的。它是如何工作的:https://pl.kotl.in/ufZRQSa7osuspendCancellableCoroutine和invokeOnCancellation如果您通过suspendCoroutine方法将回调转换为协程,那么您应该改用suspendCancellableCoroutine方法。可以使用continuation.invokeOnCancellation来执行取消:suspendfunwork(){returnsuspendCancellableCoroutine{continuation->continuation.invokeOnCancellation{//处理清理工作}//剩余实现代码}为了享受结构化并发的好处,保证我们如果没有进行多余的操作,需要保证代码可以取消。使用Jetpack中定义的CoroutineScopes:viewModelScope或lifecycleScope,它们会在作用域完成后取消它们处理的任务。如果您正在创建自己的CoroutineScope,请确保将其绑定到作业并在需要时调用取消。协程代码取消需要协同,所以更新你的代码以延迟的方式检查协程取消,避免不必要的操作。现在,大家对本系列第一部分的一些基本概念有所了解,第二部分协程的取消。在下一篇文章中,我们将继续探索和学习异常处理的第三部分。感兴趣的读者,请继续关注我们获取最新消息。【本文为栏目组织《GoogleDevelopers》原创稿件,转载请联系原作者(微信公众号:Google_Developers)】点此查看更多本作者好文
