deferred.await()在runBlocking中抛出的异常,即使在捕获后也被视为未处理

Mar*_*nik 8 kotlin kotlin-coroutines

这段代码:

fun main() {
    runBlocking {
        try {
            val deferred = async { throw Exception() }
            deferred.await()
        } catch (e: Exception) {
            println("Caught $e")
        }
    }
    println("Completed")
}
Run Code Online (Sandbox Code Playgroud)

结果输出:

Caught java.lang.Exception
Exception in thread "main" java.lang.Exception
    at org.mtopol.TestKt$main$1$deferred$1.invokeSuspend(test.kt:11)
    ...
Run Code Online (Sandbox Code Playgroud)

这种行为对我没有意义.异常被捕获并处理,但它仍然作为未处理的异常逃到顶层.

这种行为是否有记录和预期?它违反了我对异常处理如何工作的所有直觉.

我从Kotlin论坛的一个帖子中改编了这个问题.


Kotlin文档建议使用,supervisorScope如果我们不想在一个失败时取消所有协同程序.所以我可以写

fun main() {
    runBlocking {
        supervisorScope {
            try {
                launch {
                    delay(1000)
                    println("Done after delay")
                }
                val job = launch {
                    throw Exception()
                }
                job.join()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
    }
    println("Completed")
}
Run Code Online (Sandbox Code Playgroud)

输出现在

Exception in thread "main" java.lang.Exception
    at org.mtopol.TestKt$main$2$1$job$1.invokeSuspend(test.kt:16)
    ...
    at org.mtopol.TestKt.main(test.kt:8)
    ...

Done after delay
Completed
Run Code Online (Sandbox Code Playgroud)

这再次不是我想要的行为.在这里,一个launched协程失败了一个未处理的异常,使其他协同程序的工作无效,但它们不间断地进行.

我认为合理的行为是当协程以不可预见的(即未处理的)方式失败时传播取消.捕获异常await意味着没有任何全局错误,只是作为业务逻辑的一部分处理的本地化异常.

Mar*_*nik 7

在研究了 Kotlin 引入这种行为的原因后,我发现,如果异常不以这种方式传播,编写及时被取消的行为良好的代码会很复杂。例如:

runBlocking {
    val deferredA = async {
        Thread.sleep(10_000)
        println("Done after delay")
        1
    }
    val deferredB = async<Int> { throw Exception() }
    println(deferredA.await() + deferredB.await())
}
Run Code Online (Sandbox Code Playgroud)

因为a是我们碰巧等待的第一个结果,此代码将继续运行 10 秒,然后导致错误并且没有实现任何有用的工作。在大多数情况下,我们希望在一个组件出现故障时立即取消所有内容。我们可以这样做:

val (a, b) = awaitAll(deferredA, deferredB)
println(a + b)
Run Code Online (Sandbox Code Playgroud)

这段代码不太优雅:我们被迫在同一个地方等待所有结果,我们失去了类型安全,因为awaitAll返回所有参数的公共超类型的列表。如果我们有一些

suspend fun suspendFun(): Int {
    delay(10_000)
    return 2
}
Run Code Online (Sandbox Code Playgroud)

我们想写

val c = suspendFun()
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b + c)
Run Code Online (Sandbox Code Playgroud)

我们被剥夺了在suspendFun完成之前纾困的机会。我们可能会这样解决:

val deferredC = async { suspendFun() }
val (a, b, c) = awaitAll(deferredA, deferredB, deferredC)
println(a + b + c)
Run Code Online (Sandbox Code Playgroud)

但这很脆弱,因为您必须注意确保对每个可挂起的调用都执行此操作。这也违背了 Kotlin 的“默认顺序”原则

总而言之:当前的设计虽然起初违反直觉,但作为一种实用的解决方案确实有意义。它还加强了不使用的规则,async-await除非您正在对任务进行并行分解。