到目前為止我們看到的故事是:在 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