vs 启动调用的目标发生异常_协程中的取消和异常 | 取消操作详解
調(diào)用 cancel 方法
當(dāng)啟動(dòng)多個(gè)協(xié)程時(shí),無(wú)論是追蹤協(xié)程狀態(tài),還是單獨(dú)取消各個(gè)協(xié)程,都是件讓人頭疼的事情。不過(guò),我們可以通過(guò)直接取消協(xié)程啟動(dòng)所涉及的整個(gè)作用域 (scope) 來(lái)解決這個(gè)問(wèn)題,因?yàn)檫@樣可以取消所有已創(chuàng)建的子協(xié)程。// 假設(shè)我們已經(jīng)定義了一個(gè)作用域val job1 = scope.launch { … }val job2 = scope.launch { … }scope.cancel()取消作用域會(huì)取消它的子協(xié)程
有時(shí)候,您也許僅僅需要取消其中某一個(gè)協(xié)程,比如用戶輸入了某個(gè)事件,作為回應(yīng)要取消某個(gè)進(jìn)行中的任務(wù)。如下代碼所示,調(diào)用 job1.cancel 會(huì)確保只會(huì)取消跟 job1 相關(guān)的特定協(xié)程,而不會(huì)影響其余兄弟協(xié)程繼續(xù)工作。// 假設(shè)我們已經(jīng)定義了一個(gè)作用域val job1 = scope.launch { … }val job2 = scope.launch { … }?// 第一個(gè)協(xié)程將會(huì)被取消,而另一個(gè)則不受任何影響job1.cancel()被取消的子協(xié)程并不會(huì)影響其余兄弟協(xié)程
協(xié)程通過(guò)拋出一個(gè)特殊的異常 CancellationException 來(lái)處理取消操作。在調(diào)用 .cancel 時(shí)您可以傳入一個(gè) CancellationException 實(shí)例來(lái)提供更多關(guān)于本次取消的詳細(xì)信息,該方法的簽名如下:fun cancel(cause: CancellationException? = null)如果您不構(gòu)建新的 CancellationException 實(shí)例將其作為參數(shù)傳入的話,會(huì)創(chuàng)建一個(gè)默認(rèn)的 CancellationException (請(qǐng)查看完整代碼)。public override fun cancel(cause: CancellationException?) { cancelInternal(cause ?: defaultCancellationException())}- 完整代碼https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/JobSupport.kt#L612
viewModelScope
https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope
lifecycleScope
https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#lifecyclescope
為什么協(xié)程處理的任務(wù)沒(méi)有停止?
如果我們僅是調(diào)用了 cancel 方法,并不意味著協(xié)程所處理的任務(wù)也會(huì)停止。如果您使用協(xié)程處理了一些相對(duì)較為繁重的工作,比如讀取多個(gè)文件,那么您的代碼不會(huì)自動(dòng)就停止此任務(wù)的進(jìn)行。
讓我們舉一個(gè)更簡(jiǎn)單的例子看看會(huì)發(fā)生什么。假設(shè)我們需要使用協(xié)程來(lái)每秒打印兩次?"Hello"。我們先讓協(xié)程運(yùn)行一秒,然后將其取消。其中一個(gè)版本實(shí)現(xiàn)如下所示:
我們一步一步來(lái)看發(fā)生了什么。當(dāng)調(diào)用 launch 方法時(shí),我們創(chuàng)建了一個(gè)活躍 (active) 狀態(tài)的協(xié)程。緊接著我們讓協(xié)程運(yùn)行了 1,000 毫秒,打印出來(lái)的結(jié)果如下:Hello 0Hello 1Hello?2當(dāng) job.cancel 方法被調(diào)用后,我們的協(xié)程轉(zhuǎn)變?yōu)槿∠?(cancelling) 的狀態(tài)。但是緊接著我們發(fā)現(xiàn) Hello 3 和 Hello 4 打印到了命令行中。當(dāng)協(xié)程處理的任務(wù)結(jié)束后,協(xié)程又轉(zhuǎn)變?yōu)榱艘讶∠?(cancelled) 狀態(tài)。協(xié)程所處理的任務(wù)不會(huì)僅僅在調(diào)用 cancel 方法時(shí)就停止,相反,我們需要修改代碼來(lái)定期檢查協(xié)程是否處于活躍狀態(tài)。讓您的協(xié)程可以被取消
您需要確保所有使用協(xié)程處理任務(wù)的代碼實(shí)現(xiàn)都是協(xié)作式的,也就是說(shuō)它們都配合協(xié)程取消做了處理,因此您可以在任務(wù)處理期間定期檢查協(xié)程是否已被取消,或者在處理耗時(shí)任務(wù)之前就檢查當(dāng)前協(xié)程是否已取消。例如,如果您從磁盤(pán)中獲取了多個(gè)文件,在開(kāi)始讀取文件內(nèi)容之前,先檢查協(xié)程是否被取消了。類似這樣的處理方式,您可以避免處理不必要的 CPU 密集型任務(wù)。
val job = launch {????for(file?in?files)?{ // TODO 檢查協(xié)程是否被取消 readFile(file) }}所有 kotlinx.coroutines 中的掛起函數(shù) (withContext, delay 等) 都是可取消的。如果您使用它們中的任一個(gè)函數(shù),都不需要檢查協(xié)程是否已取消,然后停止任務(wù)執(zhí)行,或是拋出 CancellationException 異常。但是,如果沒(méi)有使用這些函數(shù),為了讓您的代碼能夠配合協(xié)程取消,可以使用以下兩種方法:- 檢查 job.isActive 或者使用 ensureActive()
使用 yield() 來(lái)讓其他任務(wù)進(jìn)行
檢查 job 的活躍狀態(tài)
先看一下第一種方法,在我們的 while(i<5) 循環(huán)中添加對(duì)于協(xié)程狀態(tài)的檢查:// 因?yàn)樘幱?launch 的代碼塊中,可以訪問(wèn)到 job.isActive 屬性while (i < 5 && isActive)這樣意味著我們的任務(wù)只會(huì)在協(xié)程處于活躍的狀態(tài)下執(zhí)行。同樣,這也意味著在 while 循環(huán)之外,我們?nèi)暨€想處理別的行為,比如在 job 被取消后打日志出來(lái),那就可以檢查 !isActive 然后再繼續(xù)進(jìn)行相應(yīng)的處理。Coroutine 的代碼庫(kù)中還提供了另一個(gè)很有用的方法 —— ensureActive(),它的實(shí)現(xiàn)如下:
fun Job.ensureActive(): Unit { if (!isActive) { throw getCancellationException() }}?如果 job 處于非活躍狀態(tài),這個(gè)方法會(huì)立即拋出異常,我們可以在 while 循環(huán)開(kāi)始就使用這個(gè)方法。
while (i < 5) { ensureActive() …}通過(guò)使用 ensureActive 方法,您可以避免使用 if 語(yǔ)句來(lái)檢查 isActive 狀態(tài),這樣可以減少樣板代碼的使用量,但是相應(yīng)地也失去了處理類似于日志打印這種行為的靈活性。
使用 yield() 函數(shù)運(yùn)行其他任務(wù)
如果要處理的任務(wù)屬于 1) CPU 密集型,2) 可能會(huì)耗盡線程池資源,3) 需要在不向線程池中添加更多線程的前提下允許線程處理其他任務(wù),那么請(qǐng)使用 yield()。如果 job 已經(jīng)完成,由 yield 所處理的首要任務(wù)將會(huì)是檢查任務(wù)的完成狀態(tài),完成的話則直接通過(guò)拋出 CancellationException 來(lái)退出協(xié)程。yield 可以作為定期檢查所調(diào)用的第一個(gè)函數(shù),例如上面提到的 ensureActive() 方法。yield()
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
Job.join ??Deferred.await cancellation
等待協(xié)程處理結(jié)果有兩種方法:?來(lái)自 launch 的 job 可以調(diào)用 join 方法,由 async 返回的 Deferred (其中一種 job 類型) 可以調(diào)用 await 方法。Job.join 會(huì)掛起協(xié)程,直到任務(wù)處理完成。與 job.cancel 一起使用時(shí),會(huì)按照以下方式進(jìn)行:- 如果您調(diào)用? job.cancel 之后再調(diào)用 job.join,那么協(xié)程會(huì)在任務(wù)處理完成之前一直處于掛起狀態(tài);
在 job.join 之后調(diào)用 job.cancel 沒(méi)有什么影響,因?yàn)?job 已經(jīng)完成了。
Job.join
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html
Deferred
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
Deferred.await
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
在已取消的 deferred 上調(diào)用 await 會(huì)拋出 JobCancellationException 異常。
val deferred = async { … }deferred.cancel()val?result?=?deferred.await()?//?拋出?JobCancellationException?異常為什么會(huì)拿到這個(gè)異常呢?await 的角色是負(fù)責(zé)在協(xié)程處理結(jié)果出來(lái)之前一直將協(xié)程掛起,因?yàn)槿绻麉f(xié)程被取消了那么協(xié)程就不會(huì)繼續(xù)進(jìn)行計(jì)算,也就不會(huì)有結(jié)果產(chǎn)生。因此,在協(xié)程取消后調(diào)用 await 會(huì)拋出 JobCancellationException 異常: 因?yàn)?Job 已被取消。
另一方面,如果您在 deferred.cancel 之后調(diào)用 deferred.await 不會(huì)有任何情況發(fā)生,因?yàn)閰f(xié)程已經(jīng)處理結(jié)束。
處理協(xié)程取消的副作用
假設(shè)您要在協(xié)程取消后執(zhí)行某個(gè)特定的操作,比如關(guān)閉可能正在使用的資源,或者是針對(duì)取消需要進(jìn)行日志打印,又或者是執(zhí)行其余的一些清理代碼。我們有好幾種方法可以做到這一點(diǎn):
檢查 !isActive
如果您定期地進(jìn)行 isActive 的檢查,那么一旦您跳出 while 循環(huán),就可以進(jìn)行資源的清理。之前的代碼可以更新至如下版本:
while?(i?5?&&?isActive)?{ if (…) { println(“Hello ${i++}”) nextPrintTime += 500L }}?// 協(xié)程所處理的任務(wù)已經(jīng)完成,因此我們可以做一些清理工作println(“Clean?up!”)您可以查看完整版本。完整版本
https://pl.kotl.in/loI9DaIYj
Try catch finally
因?yàn)楫?dāng)協(xié)程被取消后會(huì)拋出 CancellationException 異常,我們可以將掛起的任務(wù)放置于 try/catch 代碼塊中,然后在 finally 代碼塊中執(zhí)行需要做的清理任務(wù)。val job = launch { try { work() } catch (e: CancellationException){ println(“Work cancelled!”) } finally { println(“Clean up!”) }}delay(1000L)println(“Cancel!”)job.cancel()println(“Done!”)但是,一旦我們需要執(zhí)行的清理工作也掛起了,那上述代碼就不能夠繼續(xù)工作了,因?yàn)橐坏﹨f(xié)程處于取消中狀態(tài),它將不能再轉(zhuǎn)為掛起 (suspend) 狀態(tài)。您可以查看完整代碼。- 完整代碼https://pl.kotl.in/wjPINnWfG
當(dāng)協(xié)程被取消后需要調(diào)用掛起函數(shù),我們需要將清理任務(wù)的代碼放置于 NonCancellable CoroutineContext 中。這樣會(huì)掛起運(yùn)行中的代碼,并保持協(xié)程的取消中狀態(tài)直到任務(wù)處理完成。
val job = launch { try { work() } catch (e: CancellationException){ println(“Work cancelled!”) } finally { withContext(NonCancellable){ delay(1000L) // 或一些其他的掛起函數(shù) println(“Cleanup done!”) } }}delay(1000L)println(“Cancel!”)job.cancel()println(“Done!”)您可以查看其工作原理。- 工作原理https://pl.kotl.in/ufZRQSa7o
suspendCancellableCoroutine 和 invokeOnCancellation
如果您通過(guò) suspendCoroutine 方法將回調(diào)轉(zhuǎn)為協(xié)程,那么您更應(yīng)該使用 suspendCancellableCoroutine 方法。可以使用 continuation.invokeOnCancellation 來(lái)執(zhí)行取消操作:
suspend fun work() { return suspendCancellableCoroutine { continuation ->???????continuation.invokeOnCancellation?{? // 處理清理工作???????} // 剩余的實(shí)現(xiàn)代碼}為了享受到結(jié)構(gòu)化并發(fā)帶來(lái)的好處,并確保我們并沒(méi)有進(jìn)行多余的操作,那么需要保證代碼是可被取消的。
使用在 Jetpack: viewModelScope 或者 lifecycleScope 中定義的 CoroutineScopes,它們?cè)?scope 完成后就會(huì)取消它們處理的任務(wù)。如果要?jiǎng)?chuàng)建自己的 CoroutineScope,請(qǐng)確保將其與 job 綁定并在需要時(shí)調(diào)用 cancel。
協(xié)程代碼的取消需要是協(xié)作式的,因此請(qǐng)將代碼更新為對(duì)協(xié)程的取消操作以延后的方式進(jìn)行檢查,并避免不必要的操作。
現(xiàn)在,大家了解了本系列的第一部分協(xié)程的一些基本概念、第二部分協(xié)程的取消,在接下來(lái)的文章中,我們將繼續(xù)深入探討學(xué)習(xí)第三部分異常處理,感興趣的讀者請(qǐng)繼續(xù)關(guān)注我們的更新。
推薦閱讀
點(diǎn)擊屏末?|?閱讀原文?|?查看 Android 官方中文文檔 —— 使用 Kotlin 更快地編寫(xiě)更出色的 Android 應(yīng)用總結(jié)
以上是生活随笔為你收集整理的vs 启动调用的目标发生异常_协程中的取消和异常 | 取消操作详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: k8s查看pod的yaml文件_K8s-
- 下一篇: python连接mysql用哪个模块_P