協程的取消
本文討論協程的取消, 以及實作時可能會碰到的幾個問題.
本文屬于合輯: 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版本:
- Cancellation and timeouts
- Cancelling and timeouts github md version
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

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/554645.html
標籤:Android
上一篇:盤點| 三種移動跨平臺方案
下一篇:返回列表