Featured image of post 协程中子协程异常处理

协程中子协程异常处理

本文通过案例来熟悉子协程到父协程之间的异常是如何处理的。

案例一:子协程异常,父协程handler捕捉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private fun demo1() {
    val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d(TAG, "onCreate: CoroutineExceptionHandler:${throwable.message}")
    }
    GlobalScope.launch(handler) {
        Log.d(TAG, "onCreate: parentJob start")
        withContext(Dispatchers.IO) {
            throw RuntimeException("runtime exception")
            delay(1000)
            Log.d(TAG, "onCreate: withContext end")
        }
        Log.d(TAG, "onCreate: parentJob end")
    }
}

先上日志:

1
2
com.example.coroutinescopedemo       D  onCreate: parentJob start
com.example.coroutinescopedemo       D  onCreate: CoroutineExceptionHandler:withContext runtime exception

结论:异常能被launch指定的handler所捕捉。
分析: launch启动的协程用到的coroutineScope是一个StandaloneCoroutine,withContext启动的协程对应的coroutineScope是一个DispatchedCoroutine(因为父context和子context的dispatcher不一样,所以创建的是DispatchedCoroutine)。 在withContext中发生异常的时候,首先会回调到DispatchedCoroutine的resumeWith,最终会走到finalizeFinishingState方法,该方法里面会判断是否存在异常,如果有异常会调用cancelParent方法:

finalizeFinishingState判断异常的逻辑.png

cancelParent处理结果.png

可以看到如果 isScopedCoroutinetruecancelParent 会直接返回 true。这里的 true 不是表示继续调用 parent.childCancelled 交给父 Job,而是表示当前 scoped coroutine 已经“处理”了这个异常:它不会触发自己的 handleJobException,也不会走自己 context 里的 CoroutineExceptionHandler,而是把异常作为挂起函数的失败结果抛回调用它的挂起点。

在这个例子里,withContext(Dispatchers.IO) 对应的是 DispatchedCoroutine,它属于 scoped coroutine,所以异常会先抛回外层 launch 中调用 withContext 的位置。外层 launch 没有用 try-catch 接住这个异常,于是 launch 自己异常完成。GlobalScope.launch(handler) 创建的是根 StandaloneCoroutine,没有普通父 Job 再接手,最终会走到自己的 handleJobException,再从 context 里找到前面传入的 CoroutineExceptionHandler,所以日志里能看到 CoroutineExceptionHandler:runtime exception

总结:这个异常不是被 withContext 自己的 handler 处理,也不是 withContext 继续通过 parent.childCancelled 交给父 Job。真实流程是:withContext 内部抛异常 → scoped coroutine 不走自己的 handleJobException → 异常抛回外层 launch 的挂起点 → 外层 launch 因异常失败 → 根 StandaloneCoroutinehandleJobExceptionGlobalScope.launch(handler) 上的 CoroutineExceptionHandler 兜底处理。

案例二:子协程异常,子协程try-catch捕捉

上面代码如果把withContext中的异常通过try-catch住,父job就收不到该异常了: 子协程try-catch住的例子.png 子协程try住后的日志.png 子协程把异常catch住后,父协程的handler捕捉不到异常,并且父协程的invokeOnCompletion收不到异常,父协程之后的代码也能正常执行。因为在子协程对应的SuspendLambda中的invokeSuspend方法给try-catch住,不会把异常往上抛。

案例三:子协程的handler无法捕捉

子协程给context传递coroutineExceptionHandler: 子协程中的context传递handler的例子.png 子协程传递handler的日志.png

这里容易误解:handler 传到 withContext 的 context 里,并不代表它一定会回调。withContext 创建的是 DispatchedCoroutine,它属于 scoped coroutine,isScopedCoroutine=true 时,cancelParent 会直接返回 true,自己的 handleJobException 不会触发,所以 context 里的 CoroutineExceptionHandler 也没有机会执行。

这个异常会作为 withContext 这个挂起点的失败结果抛回外层 launch。外层没有 try-catch,所以父协程异常完成,invokeOnCompletion 能收到原始异常,后面的 onCreate: parentJob end 也不会执行。

结论:案例三的子 handler 没执行,原因在 withContext 的异常传播路径:先回到调用它的父协程挂起点,再由父协程处理。想在这个位置消费异常,用 try-catch 包住 withContext;想让子协程自己兜底,通常要把它放在 supervisorScope / SupervisorJob 这类不会向上取消父级的关系里,后面的案例会继续看。

案例四:子协程抛CancellationException,父协程无法捕捉

当子协程抛的是CancellationException,父协程捕捉不到该异常: 子协程抛CancellationException异常的例子.png 子协程抛CancelationException的日志.png

这里 handler 没有回调,程序也没有崩溃。原因是 CancellationException 在协程里不是普通失败,而是取消信号。

这个例子里的 withContext(Dispatchers.IO) 对应的是 DispatchedCoroutine,它属于 scoped coroutine。内部抛出 CancellationException 后,会先进入 withContext 自己的完成流程;因为 isScopedCoroutine=truecancelParent 直接返回 true,不会触发 withContext 自己的 handleJobException,也不会走 withContext context 里的 CoroutineExceptionHandler。接着这个 CancellationException 会作为 withContext 的失败结果,抛回外层 launch 调用 withContext 的挂起点。

外层 launch 收到的是 CancellationException,所以它会按取消完成处理,而不是按普通异常失败处理。后续在 JobSupport.cancelParent 中会判断 cause is CancellationException,并直接返回 true,因此不会继续走 StandaloneCoroutine.handleJobException,handler 也就不会回调。

日志里能看到 job exception:cancel exception,是因为 invokeOnCompletion 会收到这个取消原因;看不到 CoroutineExceptionHandler 日志,是因为 CancellationException 不会按未捕获异常兜底处理。

总结:CancellationException 的意义是告诉协程“这次是取消,不是业务失败”。它可以让协程停止,并把取消原因传给 invokeOnCompletion,但不会进入 CoroutineExceptionHandler。如果业务代码 catch 住了它,通常要重新抛出,否则可能破坏协程取消传播。

案例五:子协程抛异常,父协程分发到其它子协程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private fun demo2() {
    val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d(TAG, "handler:${throwable.message}")
    }
    val job = Job()
    val coroutineScope = CoroutineScope(job + handler)
    val job1 = coroutineScope.launch {
        delay(100)
        Log.d(TAG, "job1 end")
        throw RuntimeException("runtime exception")
    }
    job1.invokeOnCompletion {
        Log.d(TAG, "job1 invokeOnCompletion:${it?.message}")
    }
    val job2 = coroutineScope.launch {
        delay(200)
        Log.d(TAG, "job2 end")
    }
    job2.invokeOnCompletion {
        Log.d(TAG, "job2 invokeOnCompletion:${it?.message}")
    }
    val job3 = coroutineScope.launch {
        delay(300)
        Log.d(TAG, "job3 end")
    }
    job3.invokeOnCompletion {
        Log.d(TAG, "job3 invokeOnCompletion:${it?.javaClass?.simpleName}")
        Log.d(TAG, "job3 invokeOnCompletion:${it?.message}")
    }
}

日志如下:

1
2
3
4
5
6
com.example.coroutinescopedemo       D  job1 end
com.example.coroutinescopedemo       D  job2 invokeOnCompletion:Parent job is Cancelling
com.example.coroutinescopedemo       D  job3 invokeOnCompletion:JobCancellationException
com.example.coroutinescopedemo       D  job3 invokeOnCompletion:Parent job is Cancelling
com.example.coroutinescopedemo       D  handler:runtime exception
com.example.coroutinescopedemo       D  job1 invokeOnCompletion:runtime exception

这个例子里,CoroutineScope(job + handler) 里的 job 是父 JobImpl,三个 launch 创建出来的是三个子 StandaloneCoroutine。父子关系由 ChildHandleNode 串起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CoroutineScope.context
        |
        v
parentJob: JobImpl
  state -> NodeList
             |
             +-- ChildHandleNode(job=parentJob, childJob=job1) <--- job1.parentHandle
             |        |
             |        v
             |      job1: StandaloneCoroutine
             |        state -> NodeList(InvokeOnCompletion, ChildContinuation)
             |
             +-- ChildHandleNode(job=parentJob, childJob=job2) <--- job2.parentHandle
             |        |
             |        v
             |      job2: StandaloneCoroutine
             |        state -> NodeList(InvokeOnCompletion, ChildContinuation)
             |
             +-- ChildHandleNode(job=parentJob, childJob=job3) <--- job3.parentHandle
                      |
                      v
                    job3: StandaloneCoroutine
                      state -> NodeList(InvokeOnCompletion, ChildContinuation)

每个子协程启动时,都会向父 JobImpl 注册一个 ChildHandleNode。这个节点的 job 指向父 Job,childJob 指向当前子 Job;子 Job 自己的 parentHandle 又指回这个 ChildHandleNode。所以父 Job 的 state 最后会变成一个 NodeList,里面挂着三个子协程对应的 ChildHandleNode

子 Job 自己也有一套 state。这里每个 Job 都注册了 invokeOnCompletion,所以会有 InvokeOnCompletion 节点;执行到 delay 挂起时,还会创建 CancellableContinuationImpl,并通过 ChildContinuation 注册到当前子 Job 上。这样父 Job 取消子 Job 时,挂起点也能收到取消信号。

job1 抛出 RuntimeException 后,它会先进入自己的完成流程,把 state 置成 Finishing,并记录原始异常。接着 job1.cancelParent 会通过 parentHandle.childCancelled 找到父 JobImpl,父 Job 收到子协程异常后,也进入取消流程。

父 Job 取消时会遍历自己 NodeList 里的 ChildHandleNode,依次调用每个 childJob.parentCancelled。所以 job2job3 会被父 Job 取消,它们拿到的是 JobCancellationException("Parent job is Cancelling"),这个异常再通过各自的 ChildContinuation 分发到挂起点,最后触发 invokeOnCompletion

job1 自己已经是异常完成,后面不会用 JobCancellationException 覆盖原始异常。它在完成流程里调用 cancelParent(RuntimeException),这个调用会通过父 JobImpl 触发其它子协程取消;由于父 JobImpl 不负责兜底处理异常,childCancelled 最终返回 false,所以 job1.cancelParent 也返回 false。接着 job1 走到自己的 handleJobException,从 context 里找到 CoroutineExceptionHandler,日志里才会打印 handler:runtime exception。最后 job1 自己的 invokeOnCompletion 收到原始的 RuntimeException

所以上面的日志顺序是:job2job3 先收到父 Job 取消带来的 JobCancellationException,然后 CoroutineExceptionHandler 收到 job1 的原始异常,最后 job1.invokeOnCompletion 收到同一个原始异常。

上面涉及到几个关键的类,把关键类的结构梳理下: alt text alt text

案例六:父Job使用SupervisorJob,异常不分发到其它子协程,异常协程自己处理异常

把案例五中的 Job() 换成 SupervisorJob()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private fun demo3() {
    val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d(TAG, "handler:${throwable.message}")
    }
    val coroutineScope = CoroutineScope(SupervisorJob() + handler)
    val job1 = coroutineScope.launch {
        delay(100)
        Log.d(TAG, "job1 end")
        throw RuntimeException("runtime exception")
    }
    job1.invokeOnCompletion {
        Log.d(TAG, "job1 invokeOnCompletion:${it?.message}")
    }
    val job2 = coroutineScope.launch {
        delay(200)
        Log.d(TAG, "job2 end")
    }
    job2.invokeOnCompletion {
        Log.d(TAG, "job2 invokeOnCompletion:${it?.message}")
    }
    val job3 = coroutineScope.launch {
        delay(300)
        Log.d(TAG, "job3 end")
    }
    job3.invokeOnCompletion {
        Log.d(TAG, "job3 invokeOnCompletion:${it?.javaClass?.simpleName}")
        Log.d(TAG, "job3 invokeOnCompletion:${it?.message}")
    }
}

alt text 这里和案例五的差异只在父 Job:Job() 换成了 SupervisorJob()。父子关系还是通过 ChildHandleNode 建立,父 Job 的 state 里也仍然挂着 job1job2job3 对应的子节点。

区别发生在 job1 抛异常后。job1.cancelParent(RuntimeException) 会通过 parentHandle.childCancelled 调到父 Job,但这次父 Job 是 SupervisorJobImpl,它的 childCancelled 直接返回 false。所以父 Job 不会因为 job1 的异常进入取消流程,也不会遍历自己的 NodeList 去取消 job2job3,后两个协程可以继续执行完。

这个 false 还有另一层含义:父 SupervisorJob 没有兜底处理 job1 的异常。于是异常回到 job1 自己的完成流程,job1 会继续走 handleJobException,从 coroutine context 里找到 CoroutineExceptionHandler。这里的 handler 是创建 CoroutineScope(SupervisorJob() + handler) 时放进 scope context 的,子 launch 会继承它,所以最后日志里能看到 handler:runtime exception

结论:SupervisorJob 只改变“子协程失败是否取消父级和兄弟协程”这条链路,不会吞掉异常。失败的 job1 还是要自己完成异常处理;只是 job2job3 不会被连带取消。

以下的案例来自于https://juejin.cn/post/7049537608262615070

案例七:try-catch在非异常协程位置,无法捕捉

案例七

结果:try-catch竟然捕获不住,程序直接抛异常了

如果想要调试的话,可以给协程拼接CoroutineName,这样在调试的时候知道job对应的context中CoroutineName。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private fun demo7() {
    val scope = CoroutineScope(Dispatchers.Main + Job())
    scope.launch(CoroutineName("job1")) {
        Log.d(TAG, "scope1:$this")
        try {
            thirdApi(this)
        } catch (e: Exception) {
            Log.d(TAG, "demo7: e:${e.message}")
        }
    }
}
private fun thirdApi(coroutineScope: CoroutineScope) {
    coroutineScope.launch(CoroutineName("job2")) {
        Log.d(TAG, "thirdApi: start")
        delay(100)
        Log.d(TAG, "thirdApi: end")
        throw NullPointerException("null pointer exception")
    }
}

这里的 try-catch 包住的是 thirdApi(this) 这次普通函数调用。thirdApi 内部只是同步执行了一次 launch,创建出 job2 后就返回了,所以 catch 能兜住的范围只有“启动 job2”这个动作。

alt text 从字节码看,try-catch 确实只套在调用 thirdApi 的位置。job2 里面真正抛异常的位置在 delay(100) 之后,那时 job1.invokeSuspend 里这段 try-catch 已经执行完了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
job1.invokeSuspend
  try {
      thirdApi(this)
          -> launch job2
          -> return Job
  } catch { ... }   // try-catch 到这里结束

100ms 后:
job2 抛 NullPointerException
  -> job2.cancelParent
  -> job1.childCancelled
  -> job1 进入 Cancelling / Finishing,记录 job2 的异常

job2 继续完成自己的收尾
  -> job2.notifyCompletion
  -> ChildCompletion.invoke
  -> job1.continueCompleting
  -> job1.finalizeFinishingState
  -> job1.handleJobException
  -> 没有 handler 就崩溃

源码里还能看到一个细节:job1 启动 job2 后,会在 job2state 里挂一个 ChildCompletion。这个节点不是给外层 try-catch 用的,它是父子 Job 完成流程用的。job2.cancelParent 会先把异常传给 job1,让 job1 记录异常并进入取消流程;等 job2 自己完成收尾时,notifyCompletion 会触发 ChildCompletion.invoke,再回到 job1.continueCompleting,让 job1 继续完成自己的异常处理。

所以这个例子里,try-catch 写在 thirdApi(this) 外面没有用。要么把 try-catch 写进 job2 的协程体里,要么让 thirdApi 暴露一个会重新抛异常的挂起函数;单纯包住 launch 调用,接不住子协程后面异步抛出来的异常。

案例八:异常job的父job处增加CoroutineExceptionHandler捕捉异常

alt text

这个例子和案例七的路径一样。异常还是从 job2 抛出,先通过 job2.cancelParent 传给父协程 job1job1 进入取消流程后记录这个异常,等 job2 自己收尾完成,ChildCompletion.invoke 再把流程带回 job1.continueCompleting,最后由 job1 走到 handleJobException

区别在于,这次 CoroutineExceptionHandler 放在了 job1 的 context 里。job1.handleJobException 查自己的 context 时能找到 handler,所以异常会在这里被兜底处理,日志里会打印 handler 的内容,程序不会崩溃。

结论:这个场景里最终处理异常的是父协程 job1,所以 handler 要放在 job1 这一层才有效。外层 try-catch 接不住 job2 后面异步抛出的异常,但 job1 context 里的 CoroutineExceptionHandler 可以兜底。

案例九:handler给到异常协程,handler捕获不到异常

alt text

这个例子和案例八刚好相反。异常还是从 job2 抛出,也还是会先通过 job2.cancelParent 传给父协程 job1。区别在于,这次 CoroutineExceptionHandler 放在了 job2 的 context 里。

job2job1 的普通子协程,不是根协程。普通子协程发生未捕获异常时,异常处理权会交给父协程,所以 job2 自己 context 里的 CoroutineExceptionHandler 不会作为最终兜底逻辑执行。后面真正走到 handleJobException 的还是父协程 job1,但 job1 的 context 里没有 handler,所以程序会崩溃。

结论:CoroutineExceptionHandler 放在普通子协程 job2 上接不住这个异常。它要放在最终处理异常的协程那一层,也就是这里的父协程 job1

案例十:增加coroutineScope,异常协程的父协程增加try-catch捕捉异常

案例十

结果:程序不会崩溃,异常会被 try-catch 捕获,CoroutineExceptionHandler 不会执行。

这个例子多了一层 coroutineScope。外层 job1 执行到 coroutineScope { ... } 时会挂起,coroutineScope 内部创建的是 ScopeCoroutine,里面启动的 job2ScopeCoroutine 的子协程。

1
2
3
job1 外层 launch
  -> coroutineScope 对应的 ScopeCoroutine
      -> job2 内层 launch

job2 抛异常时,异常先传给它的父 Job,也就是 ScopeCoroutineScopeCoroutine 进入异常完成流程后,也要等 job2 自己收尾完成,这部分还是前面提到的 ChildCompletion 机制:tryWaitForChild 会在子 Job 上挂 ChildCompletion,子 Job 完成时再调用 parent.continueCompletingalt text alt text

这里的 parent 就是 coroutineScope 对应的 ScopeCoroutine,后面继续看 ScopeCoroutine.continueCompletingafterCompletionalt text alt text

关键在 afterCompletionScopeCoroutine 会把失败结果恢复给 coroutineScope 挂起点对应的 continuation。外层 job1invokeSuspendcoroutineScope { ... } 这一行继续执行时,会在这个挂起点重新拿到异常。 alt text

所以这里的 try-catch 能接住异常,是因为它包住了 coroutineScope { ... } 这个挂起函数调用。job2 后面异步抛出的异常,最终被 coroutineScope 重新抛回当前调用点,才进入外层的 catch

结论:coroutineScope 会等待内部子协程完成,并把子协程失败结果重新抛回自己的调用点。只要 try-catch 包住的是 coroutineScope { ... },就能接住内部 launch 的异常;异常已经被 catch 消费掉,外层 CoroutineExceptionHandler 不会再执行。

案例十一:增加supervisorScope作为异常协程的作用域,异常能被异常协程的handler所捕获

案例十一

结果:程序不会崩溃,异常被内层 launch 指定的 CoroutineExceptionHandler 处理。

这个例子和案例十类似,也多了一层作用域。区别在于,案例十用的是 coroutineScope,这里用的是 supervisorScopesupervisorScope 内部创建的是 SupervisorCoroutinejob2 还是它的子协程。

1
2
3
job1 外层 launch
  -> supervisorScope 对应的 SupervisorCoroutine
      -> job2 内层 launch

job2 抛异常时,它还是会走 job2.cancelParent,把异常通知给父 Job,也就是 SupervisorCoroutine。但 SupervisorCoroutine 重写了 childCancelled,直接返回 false

这个 false 的意思不是没有父子关系,也不是异常没有通知父 Job,而是父 Job 不处理这个子协程异常。SupervisorCoroutine 不会因为 job2 失败而进入取消流程,也不会把这个异常重新抛回 supervisorScope 的挂起点。

cancelParent 返回 false 后,job2 会继续走自己的异常完成流程,最后进入 handleJobException。因为 CoroutineExceptionHandler 放在了 job2 的 context 里,所以这里能找到 handler,日志会打印内层 handler 的内容。

结论:supervisorScope 会隔离子协程失败。子协程异常会通知父 Job,但父 Job 返回 false 表示不接管,异常最终留在失败的子协程自己处理,所以放在 job2 上的 CoroutineExceptionHandler 可以生效。

案例十二:coroutineScope内部异常未被try-catch捕获,外层handler生效

案例十二

这个例子还是 coroutineScope 这一条路径。job2 抛异常后,异常先传给 coroutineScope 对应的 ScopeCoroutineScopeCoroutinejob2 收尾完成后,会通过 afterCompletion 把失败结果恢复给 coroutineScope 的挂起点。

和案例十不同的是,这里外层没有 try-catch 包住 coroutineScope { ... }。异常从挂起点恢复回来后,会继续从 job1.invokeSuspend 抛出去,job1 自己也会以异常完成。

后面就回到了普通 launch 的未捕获异常处理流程:job1 走到 handleJobException,从自己的 context 里找到外层传入的 CoroutineExceptionHandler,于是日志会打印外层 handler 的内容,程序不会崩溃。

1
2
3
4
5
6
7
job2 抛异常
  -> ScopeCoroutine 记录异常
  -> ScopeCoroutine.afterCompletion
  -> 异常恢复到 coroutineScope 挂起点
  -> job1.invokeSuspend 没有 try-catch,继续抛出
  -> job1.handleJobException
  -> 外层 CoroutineExceptionHandler 处理

结论:coroutineScope 负责把内部子协程异常重新抛回调用点;如果调用点外面有 try-catch,就像案例十那样被 catch 消费;如果没有 try-catch,异常会继续让外层 launch 失败,最后由外层 launch context 里的 CoroutineExceptionHandler 兜底。

案例十三:supervisorScope隔离子协程异常,内部handler生效

案例十三

这个例子可以和案例十二放在一起看。案例十二用的是 coroutineScopejob2 的异常会通过 ScopeCoroutine.afterCompletion 重新抛回调用点,最后让外层 job1 失败,所以外层 handler 生效。

这里中间换成了 supervisorScopejob2 抛异常后,还是会先调用 job2.cancelParent 通知父 Job,也就是 SupervisorCoroutine。但 SupervisorCoroutine.childCancelled 返回 false,表示它不接管这个子协程异常。

1
2
3
4
5
6
7
job2 抛异常
  -> job2.cancelParent
  -> SupervisorCoroutine.childCancelled 返回 false
  -> SupervisorCoroutine 不失败
  -> 异常留给 job2 自己处理
  -> job2.handleJobException
  -> 内层 CoroutineExceptionHandler 处理

所以外层 job1 不会因为 job2 失败而异常完成,外层 handler 也不会执行。真正进入 handleJobException 的是 job2,而 job2 的 context 里有内层 CoroutineExceptionHandler,所以异常被内层 handler 处理。

结论:supervisorScope 会把子协程失败隔离在子协程自己身上。即使外层 launch 也有 handler,job2 的异常也不会像 coroutineScope 那样重新抛回外层调用点;最终生效的是失败子协程 job2 context 里的 CoroutineExceptionHandler

总结

父子 Job 关系:父子协程关系是通过 Job 建起来的。子协程初始化时,会把自己注册到父 Job 上,父 JobNodeList 里挂的是一个个 ChildHandleNode,每个 ChildHandleNode 指向对应的子 Job;子 Job 自己也会通过 parentHandle 持有这个节点。父协程取消子协程、等待子协程完成、接收子协程异常,都是基于这层关系完成的。

如果子协程正在 delay 这种挂起点上,还会有一层挂起点自己的取消注册。delay 会创建 CancellableContinuationImpl,并通过 ChildContinuation 挂到当前子 Job 上。这样父 Job 取消子 Job 时,不只是 Job 状态变了,挂起点也能收到取消信号。

异常处理:普通子协程抛出非 CancellationException 时,会先把自己的状态置为异常完成流程,并通过 cancelParent 通知父 Job。普通父 Job 接管这个异常后,会进入取消流程,取消其它子协程,等所有子协程收尾完成后,再继续完成自己的异常处理。最后真正走到 handleJobException 的,通常是接管异常的那一层协程。

CoroutineExceptionHandler 是最后兜底用的,只处理没有被上层消费掉的异常。普通子协程上的 handler 往往不生效,因为异常会先交给父协程处理;handler 要放在最终处理异常的那一层,比如外层 launch,或者被 supervisorScope 隔离后的失败子协程自己身上。

几个结论可以直接记住

  1. try-catch 包住普通 launch 调用,接不住子协程后面异步抛出的异常。
  2. withContextcoroutineScope 这类挂起函数会把内部异常重新抛回调用点,所以外层 try-catch 可以接住。
  3. coroutineScope 内部子协程失败,会让这个 scope 失败;如果调用点没有 try-catch,异常会继续让外层协程失败,最后可能由外层 handler 处理。
  4. supervisorScope / SupervisorJob 会隔离子协程失败。子协程异常仍然会通知父 Job,但父 Job 返回 false 表示不接管,异常留给失败的子协程自己处理。
  5. CancellationException 表示取消,不按普通失败处理,也不会触发 CoroutineExceptionHandler
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计