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 读书会


<<:  Day 14 - 安装与执行 YOLO

>>:  [Day 02] 为什麽要用 Kubernetes?

JavaScript入门 Day01_介绍

因为上一个自我挑战,我耍白痴,打完忘记按发表,所以只能再重新ㄌ呜呜 希望我这次不会再耍白痴了? 嘎油...

Day 18:「极速开发」- Vitawind

「闪电 + 疾风的组合吗? 不错不错!」 既然我们之前都说了要用 Vue + Tailwind 来...

Day.1 起点 - 前言 ( Percona Server )

在这30天的文章中分享藉由在工作上学习资料库管理相关的一些心得笔记与一些经验分享和操作纪录,希望能...

[Day 23] Crypto 小生活

我们依旧继续解题 Crypto Binary我不敢解,我俗辣 la cifra de (200 po...

初学者跪着学JavaScript Day7 : 资料型别 : Symbol

一日客家话:中文:茄子 客语:雕吹 当作是一种语言扩充机制 primitive data type ...