從 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 的生命週期。
  • 透過 isActiveisCompletedisCancelled 可以檢查目前狀態。
  • 由 Job() 管理的 scope,只要有一個子 Coroutine 拋出例外,scope 中還沒完成的工作都會被取消。
  • Job() 回傳的是 CompletableJob,可以呼叫 complete() 主動結束整個 Job。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10263923