到目前為止我們看到的故事是:在 Job() 管理的 scope 裡,一個子 Coroutine 出錯,剩下的兄弟都會被一起取消。這個行為對「缺一不可」的任務組合很合理,但實務上也有不少場景,每個任務其實是獨立的——某一個失敗不該影響其他的。SupervisorJob 就是為了這種情境而存在。
用 Job() 的取消傳播#
先回顧一下 Day 9 的範例。建立 scope 時用 Job() 包進 Coroutine context:
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") }
}suspend2() 拋出例外後:
- 排在後面的
suspend3()不會執行。 - 同層的第二個 launch(
work2)也不會執行,被 Job 取消。
例外從子 Coroutine 往上傳給父 Job,父 Job 把整個 scope 標記為失敗,未完成的兄弟都被一併取消。
換成 SupervisorJob 的差別#
把 Job() 換成 SupervisorJob():
class Day9 {
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job + CoroutineExceptionHandler { _, _ -> })
suspend fun doWork() {
with(scope) {
launch {
println("work1")
suspend1()
suspend2()
suspend3()
}.join()
launch {
println("work2")
}.join()
}
}
// ...
}執行結果的關鍵差異:
- 第一個 launch 仍然在
suspend2()拋例外後結束,suspend3()一樣不會執行。 - 第二個 launch(
work2)這次會被執行,不會因為兄弟的失敗而被取消。
SupervisorJob 的核心特性是:子 Coroutine 之間的失敗彼此獨立。一個子 Coroutine 出錯,不會把例外往上傳成「整個 scope 失敗」,其他兄弟可以照常進行。
共同點:都是 CompletableJob#
Job() 與 SupervisorJob() 都實作 CompletableJob 介面,都可以呼叫 complete() 把 Job 推進 Completed 狀態。
class Day9 {
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job + CoroutineExceptionHandler { _, _ -> })
suspend fun doWork() {
with(scope) {
launch {
println("work1")
suspend1()
suspend2()
suspend3()
}.join()
job.complete()
launch {
println("work2")
}.join()
}
}
}中間呼叫 job.complete() 後,整個 Job 進入 Completed 狀態,第二個 launch 即使是 SupervisorJob 也不會被執行——這是 Job 已經結束,而不是被例外取消。
該選 Job 還是 SupervisorJob#
選擇可以從任務之間的關係來判斷:
- 任務「缺一不可」:用
Job()。其中一個失敗,其他繼續執行也沒意義,整批一起取消反而省心。 - 任務彼此獨立:用
SupervisorJob()。每一個失敗只影響自己,其他兄弟仍按計畫執行。
這個選擇也呼應 Day 10 對例外處理的觀察:例外傳播是結構化併發的一部分,而 Job 與 SupervisorJob 提供了兩種不同的傳播策略。
在 UI 層常見的應用:一個畫面上有好幾個彼此獨立的資料來源(推薦清單、廣告、使用者資訊),其中一塊 fetch 失敗,其他區塊不該跟著消失。把整個畫面的 scope 用 SupervisorJob 管理就能達到這個效果。
例外處理的搭配#
需要注意的是,SupervisorJob 只改變「兄弟之間是否互相影響」這件事。它不會幫你「吃掉」例外。實務上仍要搭配適當的處理:
- launch 內未被攔截的例外,最終仍會走到 CoroutineExceptionHandler。
- async 的例外仍要在呼叫
await()的地方用 try-catch 接住。 - 在來源端 try-catch 是最直接的避錯方法。
小結#
- Job() 與 SupervisorJob() 都會控制 scope 的生命週期,差別在於「子 Coroutine 失敗」的傳播策略。
- Job() 會把同層的兄弟一起取消;SupervisorJob() 讓每個子 Coroutine 各自處理失敗,不互相牽連。
- 任務缺一不可選 Job();任務彼此獨立選 SupervisorJob()。
- 兩者都是 CompletableJob,可以呼叫
complete()主動結束。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10263923
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10264460