自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

協(xié)程中的取消和異常 | 取消操作詳解

移動開發(fā) Android
在日常的開發(fā)中,我們都知道應該避免不必要的任務處理來節(jié)省設備的內(nèi)存空間和電量的使用——這一原則在協(xié)程中同樣適用。

在日常的開發(fā)中,我們都知道應該避免不必要的任務處理來節(jié)省設備的內(nèi)存空間和電量的使用——這一原則在協(xié)程中同樣適用。您需要控制好協(xié)程的生命周期,在不需要使用的時候?qū)⑺∠?,這也是結(jié)構(gòu)化并發(fā)所倡導的,繼續(xù)閱讀本文來了解有關(guān)協(xié)程取消的來龍去脈。

為了能夠更好地理解本文所講的內(nèi)容,建議您首先閱讀本系列中的第一篇文章:協(xié)程中的取消和異常 | 核心概念介紹。

[[332964]]

調(diào)用 cancel 方法

當啟動多個協(xié)程時,無論是追蹤協(xié)程狀態(tài),還是單獨取消各個協(xié)程,都是件讓人頭疼的事情。不過,我們可以通過直接取消協(xié)程啟動所涉及的整個作用域 (scope) 來解決這個問題,因為這樣可以取消所有已創(chuàng)建的子協(xié)程。

  1. // 假設我們已經(jīng)定義了一個作用域 
  2.  
  3. val job1 = scope.launch { … } 
  4. val job2 = scope.launch { … } 
  5.  
  6. scope.cancel() 

1. 取消作用域會取消它的子協(xié)程

有時候,您也許僅僅需要取消其中某一個協(xié)程,比如用戶輸入了某個事件,作為回應要取消某個進行中的任務。如下代碼所示,調(diào)用 job1.cancel 會確保只會取消跟 job1 相關(guān)的特定協(xié)程,而不會影響其余兄弟協(xié)程繼續(xù)工作。

  1. // 假設我們已經(jīng)定義了一個作用域 
  2.  
  3. val job1 = scope.launch { … } 
  4. val job2 = scope.launch { … } 
  5.   
  6. // 第一個協(xié)程將會被取消,而另一個則不受任何影響 
  7. job1.cancel() 

2. 被取消的子協(xié)程并不會影響其余兄弟協(xié)程

協(xié)程通過拋出一個特殊的異常 CancellationException 來處理取消操作。在調(diào)用 .cancel 時您可以傳入一個 CancellationException 實例來提供更多關(guān)于本次取消的詳細信息,該方法的簽名如下:

  1. fun cancel(cause: CancellationException? = null) 

如果您不構(gòu)建新的 CancellationException 實例將其作為參數(shù)傳入的話,會創(chuàng)建一個默認的 CancellationException (請查看完整代碼)。

  1. public override fun cancel(cause: CancellationException?) { 
  2.     cancelInternal(cause ?: defaultCancellationException()) 

完整代碼:

https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/JobSupport.kt#L612

一旦拋出了 CancellationException 異常,您便可以使用這一機制來處理協(xié)程的取消。有關(guān)如何執(zhí)行此操作的更多信息,請參考下面的處理取消的副作用一節(jié)。

在底層實現(xiàn)中,子協(xié)程會通過拋出異常的方式將取消的情況通知到它的父級。父協(xié)程通過傳入的取消原因來決定是否來處理該異常。如果子協(xié)程因為 CancellationException 而被取消,對于它的父級來說是不需要進行其余額外操作的。

3. 不能在已取消的作用域中再次啟動新的協(xié)程

如果您使用的是 androidx KTX 庫的話,在大部分情況下都不需要創(chuàng)建自己的作用域,所以也就不需要負責取消它們。如果您是在 ViewModel 的作用域中進行操作,請使用 viewModelScope,或者如果在生命周期相關(guān)的作用域中啟動協(xié)程,那就應該使用 lifecycleScope。viewModelScope 和 lifecycleScope 都是 CoroutineScope 對象,它們都會在適當?shù)臅r間點被取消。例如,當 ViewModel 被清除時,在其作用域內(nèi)啟動的協(xié)程也會被一起取消。

  • 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é)程處理的任務沒有停止?

如果我們僅是調(diào)用了 cancel 方法,并不意味著協(xié)程所處理的任務也會停止。如果您使用協(xié)程處理了一些相對較為繁重的工作,比如讀取多個文件,那么您的代碼不會自動就停止此任務的進行。

讓我們舉一個更簡單的例子看看會發(fā)生什么。假設我們需要使用協(xié)程來每秒打印兩次 "Hello"。我們先讓協(xié)程運行一秒,然后將其取消。其中一個版本實現(xiàn)如下所示:

我們一步一步來看發(fā)生了什么。當調(diào)用 launch 方法時,我們創(chuàng)建了一個活躍 (active) 狀態(tài)的協(xié)程。緊接著我們讓協(xié)程運行了 1,000 毫秒,打印出來的結(jié)果如下:

  1. Hello 0 
  2. Hello 1 
  3. Hello 2 

當 job.cancel 方法被調(diào)用后,我們的協(xié)程轉(zhuǎn)變?yōu)槿∠?(cancelling) 的狀態(tài)。但是緊接著我們發(fā)現(xiàn) Hello 3 和 Hello 4 打印到了命令行中。當協(xié)程處理的任務結(jié)束后,協(xié)程又轉(zhuǎn)變?yōu)榱艘讶∠?(cancelled) 狀態(tài)。

協(xié)程所處理的任務不會僅僅在調(diào)用 cancel 方法時就停止,相反,我們需要修改代碼來定期檢查協(xié)程是否處于活躍狀態(tài)。

讓您的協(xié)程可以被取消

您需要確保所有使用協(xié)程處理任務的代碼實現(xiàn)都是協(xié)作式的,也就是說它們都配合協(xié)程取消做了處理,因此您可以在任務處理期間定期檢查協(xié)程是否已被取消,或者在處理耗時任務之前就檢查當前協(xié)程是否已取消。例如,如果您從磁盤中獲取了多個文件,在開始讀取文件內(nèi)容之前,先檢查協(xié)程是否被取消了。類似這樣的處理方式,您可以避免處理不必要的 CPU 密集型任務。

  1. val job = launch { 
  2.     for(file in files) { 
  3.         // TODO 檢查協(xié)程是否被取消 
  4.         readFile(file) 
  5.     } 

所有 kotlinx.coroutines 中的掛起函數(shù) (withContext, delay 等) 都是可取消的。如果您使用它們中的任一個函數(shù),都不需要檢查協(xié)程是否已取消,然后停止任務執(zhí)行,或是拋出 CancellationException 異常。但是,如果沒有使用這些函數(shù),為了讓您的代碼能夠配合協(xié)程取消,可以使用以下兩種方法:

  • 檢查 job.isActive 或者使用 ensureActive()
  • 使用 yield() 來讓其他任務進行

檢查 job 的活躍狀態(tài)

先看一下第一種方法,在我們的 while(i<5) 循環(huán)中添加對于協(xié)程狀態(tài)的檢查:

  1. // 因為處于 launch 的代碼塊中,可以訪問到 job.isActive 屬性 
  2. while (i < 5 && isActive) 

這樣意味著我們的任務只會在協(xié)程處于活躍的狀態(tài)下執(zhí)行。同樣,這也意味著在 while 循環(huán)之外,我們?nèi)暨€想處理別的行為,比如在 job 被取消后打日志出來,那就可以檢查 !isActive 然后再繼續(xù)進行相應的處理。

Coroutine 的代碼庫中還提供了另一個很有用的方法 —— ensureActive(),它的實現(xiàn)如下:

  1. fun Job.ensureActive(): Unit { 
  2.     if (!isActive) { 
  3.          throw getCancellationException() 
  4.     } 

如果 job 處于非活躍狀態(tài),這個方法會立即拋出異常,我們可以在 while 循環(huán)開始就使用這個方法。

  1. while (i < 5) { 
  2.     ensureActive() 
  3.     … 

通過使用 ensureActive 方法,您可以避免使用 if 語句來檢查 isActive 狀態(tài),這樣可以減少樣板代碼的使用量,但是相應地也失去了處理類似于日志打印這種行為的靈活性。

使用 yield() 函數(shù)運行其他任務

如果要處理的任務屬于 1) CPU 密集型,2) 可能會耗盡線程池資源,3) 需要在不向線程池中添加更多線程的前提下允許線程處理其他任務,那么請使用 yield()。如果 job 已經(jīng)完成,由 yield 所處理的首要任務將會是檢查任務的完成狀態(tài),完成的話則直接通過拋出 CancellationException 來退出協(xié)程。yield 可以作為定期檢查所調(diào)用的第一個函數(shù),例如上面提到的 ensureActive() 方法。

yield():

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html

Job.join 🆚 Deferred.await cancellation

等待協(xié)程處理結(jié)果有兩種方法: 來自 launch 的 job 可以調(diào)用 join 方法,由 async 返回的 Deferred (其中一種 job 類型) 可以調(diào)用 await 方法。

Job.join 會掛起協(xié)程,直到任務處理完成。與 job.cancel 一起使用時,會按照以下方式進行:

  • 如果您調(diào)用 job.cancel 之后再調(diào)用 job.join,那么協(xié)程會在任務處理完成之前一直處于掛起狀態(tài);
  • 在 job.join 之后調(diào)用 job.cancel 沒有什么影響,因為 job 已經(jīng)完成了。

Job.join:

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html

如果您關(guān)心協(xié)程處理結(jié)果,那么應該使用 Deferred。當協(xié)程完成后,結(jié)果會由 Deferred.await返回。Deferred 是 Job 的其中一種類型,它同樣可以被取消。

  • 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 會拋出 JobCancellationException 異常。

  1. val deferred = async { … } 
  2.  
  3. deferred.cancel() 
  4. val result = deferred.await() // 拋出 JobCancellationException 異常 

為什么會拿到這個異常呢?await 的角色是負責在協(xié)程處理結(jié)果出來之前一直將協(xié)程掛起,因為如果協(xié)程被取消了那么協(xié)程就不會繼續(xù)進行計算,也就不會有結(jié)果產(chǎn)生。因此,在協(xié)程取消后調(diào)用 await 會拋出 JobCancellationException 異常: 因為 Job 已被取消。

另一方面,如果您在 deferred.cancel 之后調(diào)用 deferred.await 不會有任何情況發(fā)生,因為協(xié)程已經(jīng)處理結(jié)束。

處理協(xié)程取消的副作用

假設您要在協(xié)程取消后執(zhí)行某個特定的操作,比如關(guān)閉可能正在使用的資源,或者是針對取消需要進行日志打印,又或者是執(zhí)行其余的一些清理代碼。我們有好幾種方法可以做到這一點:

1. 檢查 !isActive

如果您定期地進行 isActive 的檢查,那么一旦您跳出 while 循環(huán),就可以進行資源的清理。之前的代碼可以更新至如下版本:

  1. while (i < 5 && isActive) { 
  2.     if (…) { 
  3.         println(“Hello ${i++}”) 
  4.         nextPrintTime += 500L 
  5.     } 
  6.   
  7. // 協(xié)程所處理的任務已經(jīng)完成,因此我們可以做一些清理工作 
  8. println(“Clean up!”) 

您可以查看完整版本。

完整版本:https://pl.kotl.in/loI9DaIYj

所以現(xiàn)在,當協(xié)程不再處于活躍狀態(tài),會退出 while 循環(huán),就可以處理一些清理工作了。

2. Try catch finally

因為當協(xié)程被取消后會拋出 CancellationException 異常,我們可以將掛起的任務放置于 try/catch 代碼塊中,然后在 finally 代碼塊中執(zhí)行需要做的清理任務。

  1. val job = launch { 
  2.    try { 
  3.       work() 
  4.    } catch (e: CancellationException){ 
  5.       println(“Work cancelled!”) 
  6.     } finally { 
  7.       println(“Clean up!”) 
  8.     } 
  9.  
  10. delay(1000L) 
  11. println(“Cancel!”) 
  12. job.cancel() 
  13. println(“Done!”) 

但是,一旦我們需要執(zhí)行的清理工作也掛起了,那上述代碼就不能夠繼續(xù)工作了,因為一旦協(xié)程處于取消中狀態(tài),它將不能再轉(zhuǎn)為掛起 (suspend) 狀態(tài)。您可以查看完整代碼。

完整代碼:https://pl.kotl.in/wjPINnWfG

處于取消中狀態(tài)的協(xié)程不能夠掛起

當協(xié)程被取消后需要調(diào)用掛起函數(shù),我們需要將清理任務的代碼放置于 NonCancellable CoroutineContext 中。這樣會掛起運行中的代碼,并保持協(xié)程的取消中狀態(tài)直到任務處理完成。

  1. val job = launch { 
  2.    try { 
  3.       work() 
  4.    } catch (e: CancellationException){ 
  5.       println(“Work cancelled!”) 
  6.     } finally { 
  7.       withContext(NonCancellable){ 
  8.          delay(1000L) // 或一些其他的掛起函數(shù) 
  9.          println(“Cleanup done!”) 
  10.       } 
  11.     } 
  12.  
  13. delay(1000L) 
  14. println(“Cancel!”) 
  15. job.cancel() 
  16. println(“Done!”) 

您可以查看其工作原理。

工作原理:https://pl.kotl.in/ufZRQSa7o

suspendCancellableCoroutine 和 invokeOnCancellation

如果您通過 suspendCoroutine 方法將回調(diào)轉(zhuǎn)為協(xié)程,那么您更應該使用 suspendCancellableCoroutine 方法??梢允褂? continuation.invokeOnCancellation 來執(zhí)行取消操作:

  1. suspend fun work() { 
  2.    return suspendCancellableCoroutine { continuation -> 
  3.        continuation.invokeOnCancellation {  
  4.           // 處理清理工作 
  5.        } 
  6.    // 剩余的實現(xiàn)代碼 

為了享受到結(jié)構(gòu)化并發(fā)帶來的好處,并確保我們并沒有進行多余的操作,那么需要保證代碼是可被取消的。

使用在 Jetpack: viewModelScope 或者 lifecycleScope 中定義的 CoroutineScopes,它們在 scope 完成后就會取消它們處理的任務。如果要創(chuàng)建自己的 CoroutineScope,請確保將其與 job 綁定并在需要時調(diào)用 cancel。

協(xié)程代碼的取消需要是協(xié)作式的,因此請將代碼更新為對協(xié)程的取消操作以延后的方式進行檢查,并避免不必要的操作。

現(xiàn)在,大家了解了本系列的第一部分協(xié)程的一些基本概念、第二部分協(xié)程的取消,在接下來的文章中,我們將繼續(xù)深入探討學習第三部分異常處理,感興趣的讀者請繼續(xù)關(guān)注我們的更新。

【本文是51CTO專欄機構(gòu)“谷歌開發(fā)者”的原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者(微信公眾號:Google_Developers)】

戳這里,看該作者更多好文

 

 

責任編輯:趙寧寧 來源: 51CTO專欄
相關(guān)推薦

2020-07-07 10:03:27

Android 協(xié)程開發(fā)

2017-05-02 11:38:00

PHP協(xié)程實現(xiàn)過程

2021-09-16 09:59:13

PythonJavaScript代碼

2021-04-25 09:36:20

Go協(xié)程線程

2023-11-17 11:36:59

協(xié)程纖程操作系統(tǒng)

2023-08-08 07:18:17

協(xié)程管道函數(shù)

2020-04-07 11:10:30

Python數(shù)據(jù)線程

2020-02-24 10:39:55

Python函數(shù)線程池

2021-08-04 16:19:55

AndroidKotin協(xié)程Coroutines

2024-02-05 09:06:25

Python協(xié)程Asyncio庫

2024-06-27 07:56:49

2021-09-10 17:02:43

Python協(xié)程goroutine

2022-11-21 06:55:08

golang協(xié)程

2024-10-22 15:10:49

2023-10-24 19:37:34

協(xié)程Java

2023-11-26 18:35:25

Python編程語言

2020-06-19 08:01:48

Kotlin 協(xié)程編程

2025-02-08 09:13:40

2017-09-22 16:08:16

Python協(xié)程編程

2021-12-09 06:41:56

Python協(xié)程多并發(fā)
點贊
收藏

51CTO技術(shù)棧公眾號