iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Software Development

Coroutine 停看聽系列 第 9

Day9:Job vs SupervisorJob

還記得launch 的回傳值是 Job 嗎?我們可以使用 job 的 cancel() 來取消該 Coroutine。不知道你有沒有想過 Job 是什麼東西呢?

Job()

launch 回傳出來的 Job 是一個繼承 CoroutineContext.Element 的介面,之前時常提到 coroutine context,Job 就是其中一個元素,所以在建立 CoroutineScope 的時候,我們也可以將 Job 傳入。 (我會在之後介紹 CoroutineContext)

Job 跟 coroutine 的生命週期相關,它生命週期的最後就是 completion。無論是正常結束(Completed)還是錯誤發生(Cancelled)。如下圖:

Job 的生命週期圖

Job Life-cycle

Job 提供了三個 flag 供我們使用

  • isActive
  • isCompleted
  • isCancelled

我們可以使用這三個 flag 來查看目前 Coroutine 是走到哪一個生命週期。


前面曾經提到,最外層的 Coroutine 會等待所有的子 Coroutine 完成之後,才會結束。

假如子 Coroutine 沒有辦法順利完成,並且發生錯誤時,Job 就會把後面所有的任務都取消。如下面的範例:

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()
}

建立一個 CoroutineScope 的時候,同時可以傳一個 Job() 進 CoroutineContext 的參數中。以上方的範例,我們傳入了一個 Job() 至 CoroutineScope 中,這整個 CoroutineScope 的生命週期就由 Job() 來控制。

這個範例中,在 with(scope) 裡面有兩個 launch ,我們建立好 launch 之後立刻呼叫該 join() 函式,讓他立刻執行直到裏面的任務全部完成。其中第一個 launch 裏面有三個 suspend 函式,不過很不幸的第二個 suspend 函式發生了 RuntimeException ,我們來看看會程式會怎麼執行。

Job()

→ 因為 suspend2 裡面發生了 RuntimeException ,原本在 suspend2 後面的 suspend3 就被取消不執行了。而原本在第一個 launch 之後應該要執行的第二個 launch 也被取消。

由這段程式碼我們發現,一個 Coroutine 範圍裏面的程式,如果發生了異常,那麼 Job 就會取消所有的子 coroutine ,以這個例子來說就是把 suspend3 以及第二個 launch 給取消了。

SupervisorJob()

假如我們希望在發生異常之後,第二個 launch 不會被取消,那麼我們可以把上面的 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()
        }
    }
    ...
}

SupervisorJob

→ 雖然 suspend2 發生異常,但是第二個 launch 卻不會被 Job 給取消,這是因為使用 SupervisorJob() 的時候,所有的子 coroutine 彼此的異常狀態是獨立的,不會因為其中一個任務發生異常之後就造成所有在同一個 coroutine 範圍的呼叫被取消。


Job() 與 SupervisorJob()

都是實作 CompleteableJob 介面的 Job() ,可以呼叫 complete() 來讓 Job 的 life-cycle 進入 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()
        }
		}
		...
}

complete()

→ 原本執行完第一個 launch 之後,應該要執行第二個 launch ,但是因為在中間呼叫了 job.complete() ,所以讓整個 Job 進入 Completed 的狀態,第二個 launch 也就不會執行了。

小結

每一個 CoroutineScope 都可以帶入 Job() ,根據帶入不同的 CompleableJob 實例會有不同處理異常的方式。 Job() 是會直接取消後面所有的子 coroutine 執行,而 SupervisorJob() 則是會讓每一個 coroutine 能夠自行處理異常處理。需要使用什麼 Job() 端看使用的情境。如果執行的任務缺一不可,那麼使用 Job() 就是比較合適的,因為當某一個工作發生異常之後,其他的任務就算完成也沒有意義。假設每一個任務都是獨立的話,也許 SupervisorJob() 會比較適合你。]

參考資料

Job(CoroutineContext.Element)
Job
SupervisorJob
CompletableJob


特別感謝

Kotlin Taiwan User Group
Kotlin 讀書會


上一篇
Day8:結構化併發 (Structured Concurrency)
下一篇
Day10:例外處理,留下來或我跟你走
系列文
Coroutine 停看聽30

尚未有邦友留言

立即登入留言