從 launch 拿回來的 Job,不只是「可以呼叫 cancel 的東西」。它其實是 Coroutine context 的一個元素,掌管整個 Coroutine 的生命週期。要了解結構化併發怎麼控制取消、怎麼傳播例外,得先把 Job 看清楚。
Job 是 Coroutine context 的一個元素#
從 launch 回傳的 Job,是一個繼承 CoroutineContext.Element 的介面。換句話說,它本來就屬於 Coroutine context 的一部分。在建立 CoroutineScope 時,可以把 Job 直接傳進去:
val job = Job()
val scope = CoroutineScope(Dispatchers.Default + job + CoroutineExceptionHandler { _, _ -> })整個 scope 的生命週期,就由這個 Job 控制。
生命週期與三個 flag#
Job 跟 Coroutine 的生命週期綁在一起,最終會走到完成(Completed),不論是正常結束還是被取消(Cancelled)。Job 提供三個常用的 flag 讓我們檢查目前狀態:
isActive:Coroutine 是否還在運作。isCompleted:是否已完成(含正常結束與被取消)。isCancelled:是否處於取消狀態。
這三個 flag 之後在做協作式取消(Cooperative Cancellation)、寫迴圈型任務時會頻繁用到。
Job 在出錯時的行為#
結構化併發下,父 Coroutine 會等所有子 Coroutine 完成才結束。如果其中一個子任務發生例外(CancellationException 以外的例外),由 Job 控制的 scope 會把後續所有任務一併取消。
class Day9 {
val job = Job()
val scope = CoroutineScope(Dispatchers.Default + job + CoroutineExceptionHandler { _, _ -> })
suspend fun doWork() {
with(scope) {
launch {
println("work1")
suspend1()
suspend2()
suspend3()
}.join()
launch {
println("work2")
}.join()
}
}
suspend fun suspend1() {
delay(100)
println("suspend1")
}
suspend fun suspend2() {
delay(500)
println("suspend2")
throw RuntimeException()
}
suspend fun suspend3() {
delay(200)
println("suspend3")
}
}
fun main() = runBlocking {
val day9 = Day9()
day9.doWork()
}這個範例在 with(scope) 裡有兩個 launch,每個都立刻 join()。第一個 launch 內呼叫了三個 suspend 函式,但 suspend2() 拋出了 RuntimeException。
執行結果:
suspend2拋出例外後,原本排在後面的suspend3就不會被執行。- 連帶外層的第二個 launch(執行
work2)也不會被執行,被 Job 取消掉。
換句話說,由 Job() 管理的 scope,只要一個子 Coroutine 出錯,整個 scope 內未完成的工作都會被取消。
CompletableJob 與 complete()#
Job() 這個工廠函式回傳的其實是 CompletableJob。CompletableJob 比一般 Job 多了一個 complete() 方法,可以主動把 Job 推到 Completed 狀態。
class Day9 {
val job = Job()
val scope = CoroutineScope(Dispatchers.Default + job + CoroutineExceptionHandler { _, _ -> })
suspend fun doWork() {
with(scope) {
launch {
println("work1")
}.join()
job.complete()
launch {
println("work2")
}.join()
}
}
}中間呼叫 job.complete() 後,整個 Job 進入 Completed 狀態,第二個 launch 就不會被執行。
SupervisorJob 同樣是 CompletableJob,差別在於它「不會因為某個子 Coroutine 出錯就連帶取消其他兄弟」。SupervisorJob 的特性會在後續章節單獨整理。
小結#
- Job 是 Coroutine context 的元素,掌管 Coroutine 的生命週期。
- 透過
isActive、isCompleted、isCancelled可以檢查目前狀態。 - 由 Job() 管理的 scope,只要有一個子 Coroutine 拋出例外,scope 中還沒完成的工作都會被取消。
- Job() 回傳的是 CompletableJob,可以呼叫
complete()主動結束整個 Job。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10263923