
調用 cancel 方法
當啟動多個協程時,無論是追蹤協程狀態,還是單獨取消各個協程,都是件讓人頭疼的事情。不過,我們可以通過直接取消協程啟動所涉及的整個作用域 (scope) 來解決這個問題,因為這樣可以取消所有已創建的子協程。// 假設我們已經定義了一個作用域val job1 = scope.launch { … }val job2 = scope.launch { … }scope.cancel()
取消作用域會取消它的子協程
有時候,您也許僅僅需要取消其中某一個協程,比如用戶輸入了某個事件,作為回應要取消某個進行中的任務。如下代碼所示,調用 job1.cancel 會確保只會取消跟 job1 相關的特定協程,而不會影響其余兄弟協程繼續工作。// 假設我們已經定義了一個作用域val job1 = scope.launch { … }val job2 = scope.launch { … }?// 第一個協程將會被取消,而另一個則不受任何影響job1.cancel()
被取消的子協程并不會影響其余兄弟協程
協程通過拋出一個特殊的異常 CancellationException 來處理取消操作。在調用 .cancel 時您可以傳入一個 CancellationException 實例來提供更多關于本次取消的詳細信息,該方法的簽名如下:fun cancel(cause: CancellationException? = null)
如果您不構建新的 CancellationException 實例將其作為參數傳入的話,會創建一個默認的 CancellationException (請查看完整代碼)。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
為什么協程處理的任務沒有停止?
如果我們僅是調用了 cancel 方法,并不意味著協程所處理的任務也會停止。如果您使用協程處理了一些相對較為繁重的工作,比如讀取多個文件,那么您的代碼不會自動就停止此任務的進行。
讓我們舉一個更簡單的例子看看會發生什么。假設我們需要使用協程來每秒打印兩次?"Hello"。我們先讓協程運行一秒,然后將其取消。其中一個版本實現如下所示:
Hello 0Hello 1Hello?2
當 job.cancel 方法被調用后,我們的協程轉變為取消中 (cancelling) 的狀態。但是緊接著我們發現 Hello 3 和 Hello 4 打印到了命令行中。當協程處理的任務結束后,協程又轉變為了已取消 (cancelled) 狀態。協程所處理的任務不會僅僅在調用 cancel 方法時就停止,相反,我們需要修改代碼來定期檢查協程是否處于活躍狀態。讓您的協程可以被取消
您需要確保所有使用協程處理任務的代碼實現都是協作式的,也就是說它們都配合協程取消做了處理,因此您可以在任務處理期間定期檢查協程是否已被取消,或者在處理耗時任務之前就檢查當前協程是否已取消。例如,如果您從磁盤中獲取了多個文件,在開始讀取文件內容之前,先檢查協程是否被取消了。類似這樣的處理方式,您可以避免處理不必要的 CPU 密集型任務。
val job = launch {????for(file?in?files)?{ // TODO 檢查協程是否被取消 readFile(file) }}
所有 kotlinx.coroutines 中的掛起函數 (withContext, delay 等) 都是可取消的。如果您使用它們中的任一個函數,都不需要檢查協程是否已取消,然后停止任務執行,或是拋出 CancellationException 異常。但是,如果沒有使用這些函數,為了讓您的代碼能夠配合協程取消,可以使用以下兩種方法:- 檢查 job.isActive 或者使用 ensureActive()
使用 yield() 來讓其他任務進行
檢查 job 的活躍狀態
先看一下第一種方法,在我們的 while(i<5) 循環中添加對于協程狀態的檢查:// 因為處于 launch 的代碼塊中,可以訪問到 job.isActive 屬性while (i < 5 && isActive)
這樣意味著我們的任務只會在協程處于活躍的狀態下執行。同樣,這也意味著在 while 循環之外,我們若還想處理別的行為,比如在 job 被取消后打日志出來,那就可以檢查 !isActive 然后再繼續進行相應的處理。Coroutine 的代碼庫中還提供了另一個很有用的方法 —— ensureActive(),它的實現如下:
fun Job.ensureActive(): Unit { if (!isActive) { throw getCancellationException() }}
?如果 job 處于非活躍狀態,這個方法會立即拋出異常,我們可以在 while 循環開始就使用這個方法。
while (i < 5) { ensureActive() …}
通過使用 ensureActive 方法,您可以避免使用 if 語句來檢查 isActive 狀態,這樣可以減少樣板代碼的使用量,但是相應地也失去了處理類似于日志打印這種行為的靈活性。
使用 yield() 函數運行其他任務
如果要處理的任務屬于 1) CPU 密集型,2) 可能會耗盡線程池資源,3) 需要在不向線程池中添加更多線程的前提下允許線程處理其他任務,那么請使用 yield()。如果 job 已經完成,由 yield 所處理的首要任務將會是檢查任務的完成狀態,完成的話則直接通過拋出 CancellationException 來退出協程。yield 可以作為定期檢查所調用的第一個函數,例如上面提到的 ensureActive() 方法。yield()
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
Job.join ??Deferred.await cancellation
等待協程處理結果有兩種方法:?來自 launch 的 job 可以調用 join 方法,由 async 返回的 Deferred (其中一種 job 類型) 可以調用 await 方法。Job.join 會掛起協程,直到任務處理完成。與 job.cancel 一起使用時,會按照以下方式進行:- 如果您調用? job.cancel 之后再調用 job.join,那么協程會在任務處理完成之前一直處于掛起狀態;
在 job.join 之后調用 job.cancel 沒有什么影響,因為 job 已經完成了。
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 上調用 await 會拋出 JobCancellationException 異常。
val deferred = async { … }deferred.cancel()val?result?=?deferred.await()?//?拋出?JobCancellationException?異常
為什么會拿到這個異常呢?await 的角色是負責在協程處理結果出來之前一直將協程掛起,因為如果協程被取消了那么協程就不會繼續進行計算,也就不會有結果產生。因此,在協程取消后調用 await 會拋出 JobCancellationException 異常: 因為 Job 已被取消。
另一方面,如果您在 deferred.cancel 之后調用 deferred.await 不會有任何情況發生,因為協程已經處理結束。
處理協程取消的副作用
假設您要在協程取消后執行某個特定的操作,比如關閉可能正在使用的資源,或者是針對取消需要進行日志打印,又或者是執行其余的一些清理代碼。我們有好幾種方法可以做到這一點:
檢查 !isActive
如果您定期地進行 isActive 的檢查,那么一旦您跳出 while 循環,就可以進行資源的清理。之前的代碼可以更新至如下版本:
while?(i?5?&&?isActive)?{ if (…) { println(“Hello ${i++}”) nextPrintTime += 500L }}?// 協程所處理的任務已經完成,因此我們可以做一些清理工作println(“Clean?up!”)
您可以查看完整版本。完整版本
https://pl.kotl.in/loI9DaIYj
Try catch finally
因為當協程被取消后會拋出 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!”)
但是,一旦我們需要執行的清理工作也掛起了,那上述代碼就不能夠繼續工作了,因為一旦協程處于取消中狀態,它將不能再轉為掛起 (suspend) 狀態。您可以查看完整代碼。- 完整代碼https://pl.kotl.in/wjPINnWfG
當協程被取消后需要調用掛起函數,我們需要將清理任務的代碼放置于 NonCancellable CoroutineContext 中。這樣會掛起運行中的代碼,并保持協程的取消中狀態直到任務處理完成。
val job = launch { try { work() } catch (e: CancellationException){ println(“Work cancelled!”) } finally { withContext(NonCancellable){ delay(1000L) // 或一些其他的掛起函數 println(“Cleanup done!”) } }}delay(1000L)println(“Cancel!”)job.cancel()println(“Done!”)
您可以查看其工作原理。- 工作原理https://pl.kotl.in/ufZRQSa7o
suspendCancellableCoroutine 和 invokeOnCancellation
如果您通過 suspendCoroutine 方法將回調轉為協程,那么您更應該使用 suspendCancellableCoroutine 方法。可以使用 continuation.invokeOnCancellation 來執行取消操作:
suspend fun work() { return suspendCancellableCoroutine { continuation ->???????continuation.invokeOnCancellation?{? // 處理清理工作???????} // 剩余的實現代碼}
為了享受到結構化并發帶來的好處,并確保我們并沒有進行多余的操作,那么需要保證代碼是可被取消的。
使用在 Jetpack: viewModelScope 或者 lifecycleScope 中定義的 CoroutineScopes,它們在 scope 完成后就會取消它們處理的任務。如果要創建自己的 CoroutineScope,請確保將其與 job 綁定并在需要時調用 cancel。
協程代碼的取消需要是協作式的,因此請將代碼更新為對協程的取消操作以延后的方式進行檢查,并避免不必要的操作。
現在,大家了解了本系列的第一部分協程的一些基本概念、第二部分協程的取消,在接下來的文章中,我們將繼續深入探討學習第三部分異常處理,感興趣的讀者請繼續關注我們的更新。
推薦閱讀



