# 協程的取消 本文討論協程的取消, 以及實現時可能會碰到的幾個問題. ![coroutine cancellation](https://img2023.cnblogs.com/blog/325852/202306/325852-20230607235812812-279507376.png) 本 ...
協程的取消
本文討論協程的取消, 以及實現時可能會碰到的幾個問題.
本文屬於合輯: https://github.com/mengdd/KotlinTutorials
協程的取消
取消的意義: 避免資源浪費, 以及多餘操作帶來的問題.
基本特性:
- cancel scope的時候會cancel其中的所有child coroutines.
- 一旦取消一個scope, 你將不能再在其中launch新的coroutine.
- 一個在取消狀態的coroutine是不能suspend的.
如果一個coroutine拋出了異常, 它將會把這個exception向上拋給它的parent, 它的parent會做以下三件事情:
- 取消其他所有的children.
- 取消自己.
- 把exception繼續向上傳遞.
Android開發中的取消
在Android開發中, 比較常見的情形是由於View生命周期的終止, 我們需要取消一些操作.
通常我們不需要手動調用cancel()
方法, 那是因為我們利用了一些更高級的包裝方法, 比如:
viewModelScope
: 會在ViewModel onClear的時候cancel.lifecycleScope
: 會在作為Lifecycle Owner的View對象: Activity, Fragment到達DESTROYED狀態時cancel.
取消並不是自動獲得的
all suspend functions from kotlinx.coroutines
are cancellable, but not yours.
kotlin官方提供的suspend方法都會有cancel的處理, 但是我們自己寫的suspend方法就需要自己留意.
尤其是耗時或者帶迴圈的地方, 通常需要自己加入檢查, 否則即便調用了cancel, 代碼也繼續在執行.
有這麼幾種方法:
isActive()
ensureActive()
yield()
: 除了ensureActive以外, 會出讓資源, 比如其他工作不需要再往線程池裡加線程.
一個在迴圈中檢查coroutine是否依然活躍的例子:
fun main() = runBlocking {
val startTime = currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// print a message twice a second
if (currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
輸出:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
catch Exception和runCatching
眾所周知catch一個很general的Exception
類型可能不是一個好做法.
因為你以為捕獲了A, B, C異常, 結果實際上還有D, E, F.
捕獲具體的異常類型, 在開發階段的快速失敗會幫助我們更早定位和解決問題.
協程還推出了一個"方便"的runCatching
方法, catchThrowable
.
讓我們寫出了看似更"保險", 但卻更容易破壞取消機制的代碼.
如果我們catch了CancellationException
, 會破壞Structured Concurrency.
看這個例子:
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
println("my long time function start")
myLongTimeFunction()
println("my other operations ==== ") // this line should not be printed when cancelled
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
private suspend fun myLongTimeFunction() = runCatching {
var i = 0
while (i < 10) {
// print a message twice a second
println("job: I'm sleeping ${i++} ...")
delay(500)
}
}
輸出:
my long time function start
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
my other operations ====
main: Now I can quit.
當job cancel了以後後續的工作不應該繼續進行, 然而我們可以看到log仍然被列印出來, 這是因為runCatching
把異常全都catch了.
這裡有個open issue討論這個問題: https://github.com/Kotlin/kotlinx.coroutines/issues/1814
CancellationException的特殊處理
如何解決上面的問題呢? 基本方案是把CancellationException
再throw出來.
比如對於runCatching的改造, NowInAndroid里有這麼一個方法suspendRunCatching:
private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = try {
Result.success(block())
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Log.i(
"suspendRunCatching",
"Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
exception
)
Result.failure(exception)
}
上面的例子改為用這個suspendRunCatching
方法替代runCatching
就修好了.
上面例子的輸出變為:
my long time function start
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
不想取消的處理
可能還有一些工作我們不想隨著job的取消而完全取消.
資源清理工作
finally通常用於try block之後的的資源清理, 如果其中沒有suspend方法那麼沒有問題.
如果finally中的代碼是suspend的, 如前所述, 一個在取消狀態的coroutine是不能suspend的.
那麼需要用一個withContext(NonCancellable)
.
例子:
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
註意這個方法一般用於會suspend的資源清理, 不建議在各個場合到處使用, 因為它破壞了對coroutine執行取消的控制.
需要更長生命周期的工作
如果有一些工作需要比View/ViewModel更長的生命周期, 可以把它放在更下層, 用一個生命周期更長的scope.
可以根據不同的場景設計, 比如可以用一個application生命周期的scope:
class MyApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}
再把這個scope註入到repository中去.
如果需要做的工作比application的生命周期更長, 那麼可以考慮用WorkManager
.
總結: 不要破壞Structured Concurrency
Structure Concurrency為開發者提供了方便管理多個coroutines的有效方法.
基本上破壞Structure Concurrency特性的行為(比如用GlobalScope, 用NonCancellable, catch CancellationException等)都是反模式, 要小心使用.
還要註意不要隨便傳遞job.
CoroutineContext
有一個元素是job, 但是這並不意味著我們可以像切Dispatcher一樣隨便傳一個job參數進去.
文章: Structured Concurrency Anniversary
看這裡: https://github.com/Kotlin/kotlinx.coroutines/issues/1001
References & Further Reading
Kotlin官方文檔的網頁版和markdown版本:
Android官方文檔上鏈接的博客和視頻:
- Cancellation in coroutines
- KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo
其他:
- Coroutines: first things first
- Kotlin Coroutines and Flow - Use Cases on Android
- Structured Concurrency Anniversary
- Exceptions in coroutines
- Coroutines & Patterns for work that shouldn’t be cancelled
出處: 博客園: 聖騎士Wind
Github: https://github.com/mengdd
微信公眾號: 聖騎士Wind
![微信公眾號: 聖騎士Wind](https://images.cnblogs.com/cnblogs_com/mengdd/869539/o_200422055937qrcode_for_gh_0e2ed690dcda_258.jpg)