CoroutineExceptionHandler 在作为启动上下文提供时未执行

Jul*_* A. 6 coroutine kotlin kotlinx.coroutines

当我运行这个:

fun f() = runBlocking {
    val eh = CoroutineExceptionHandler { _, e -> trace("exception handler: $e") }
    val j1 = launch(eh) {
        trace("launched")
        delay(1000)
        throw RuntimeException("error!")
    }
    trace("joining")
    j1.join()
    trace("after join")
}
f()
Run Code Online (Sandbox Code Playgroud)

这是输出:

[main @coroutine#1]: joining
[main @coroutine#2]: launched
java.lang.RuntimeException: error!
    at ExceptionHandling$f9$1$j1$1.invokeSuspend(ExceptionHandling.kts:164)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
    at kotlinx.coroutines.ResumeModeKt.resumeMode(ResumeMode.kt:67)
Run Code Online (Sandbox Code Playgroud)

根据CoroutineExceptionHandler的文档,eh应该执行我提供的处理程序。但事实并非如此。这是为什么?

Jul*_* A. 14

我相信答案就在官方协程文档的这一节中:

如果协程遇到 CancellationException 以外的异常,它会用该异常取消其父进程。此行为不能被覆盖,并用于为不依赖于 CoroutineExceptionHandler 实现的结构化并发提供稳定的协程层次结构。当所有子进程终止时,原始异常由父进程处理。

这也是为什么在这些示例中 CoroutineExceptionHandler 始终安装到在 GlobalScope 中创建的协程的原因。将异常处理程序安装到在主 runBlocking 范围内启动的协程是没有意义的,因为尽管安装了 handler ,但当其子协程以异常完成时,主协程将始终被取消

(强调我的)

此处描述的内容不仅适用于runBlockingand GlobalScope,还适用于任何非顶级协程构建器和自定义范围。

举例说明(使用 kotlinx.coroutines v1.0.0):

fun f() = runBlocking {
    val h1 = CoroutineExceptionHandler { _, e ->
        trace("handler 1 e: $e")
    }
    val h2 = CoroutineExceptionHandler { _, e ->
        trace("handler 2 e: $e")
    }
    val cs = CoroutineScope(newSingleThreadContext("t1"))
    trace("launching j1")
    val j1 = cs.launch(h1) {
        delay(1000)
        trace("launching j2")
        val j2 = launch(h2) {
            delay(500)
            trace("throwing exception")
            throw RuntimeException("error!")
        }
        j2.join()
    }
    trace("joining j1")
    j1.join()
    trace("exiting f")
}
f()
Run Code Online (Sandbox Code Playgroud)

输出:

[main @coroutine#1]: launching j1
[main @coroutine#1]: joining j1
[t1 @coroutine#2]: launching j2
[t1 @coroutine#3]: throwing exception
[t1 @coroutine#2]: handler 1 e: java.lang.RuntimeException: error!
[main @coroutine#1]: exiting f
Run Code Online (Sandbox Code Playgroud)

请注意,处理程序h1已执行,但未执行h2。这类似于GlobalScope#launch执行时的处理程序,但不是提供给任何launchinside的处理程序runBlocking

TLDR

提供给范围的非根协程的处理程序将被忽略。将执行提供给根协程的处理程序。

正如 Marko Topolnik 在下面的评论中正确指出的那样,上述概括仅适用于由launch. 由asyncor创建的那些produce将始终忽略所有处理程序。

  • 更奇怪的是:如果根协程是 `async` 或 `produce`,异常处理程序将不起作用。基本原理是你会在`await`/`receive` 中得到异常,但是在你到达这些行之前,`catch` 块并不能阻止整个范围失败。 (4认同)