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

CoroutineExceptionHandler not executed when provided as launch context

当我运行这个:

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

这是输出:

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

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

你的 kotlinx.coroutines 版本是什么?由于 0.26.0 独立 launch 构建器现已弃用,您应该改用 GlobalScope.launch

我试过你的样本,修改后它起作用了。

Kotlinx.coroutines changelog

我相信答案就在 official coroutines docs 的这一部分:

If a coroutine encounters exception other than CancellationException, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for structured concurrency which do not depend on CoroutineExceptionHandler implementation. The original exception is handled by the parent when all its children terminate.

This also a reason why, in these examples, CoroutineExceptionHandler is always installed to a coroutine that is created in GlobalScope. It does not make sense to install an exception handler to a coroutine that is launched in the scope of the main runBlocking, since the main coroutine is going to be always cancelled when its child completes with exception despite the installed handler.

(强调我的)

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

举例说明(使用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()

输出:

[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

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

内任何 launch 的处理程序

TLDR

提供给作用域的非根协程的处理程序将被忽略。将执行提供给根协程的处理程序。

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