11

这段代码:

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

导致此输出:

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

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

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

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


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

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")
}

现在的输出是

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

这又不是我想要的行为。这里一个launched 协程因未处理的异常而失败,使其他协程的工作无效,但它们不会中断。

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

4

4 回答 4

8

在研究了 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())
}

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

val (a, b) = awaitAll(deferredA, deferredB)
println(a + b)

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

suspend fun suspendFun(): Int {
    delay(10_000)
    return 2
}

我们想写

val c = suspendFun()
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b + c)

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

val deferredC = async { suspendFun() }
val (a, b, c) = awaitAll(deferredA, deferredB, deferredC)
println(a + b + c)

但这很脆弱,因为您必须注意确保为每个可暂停的呼叫执行此操作。这也违反了 Kotlin 的“默认顺序”学说

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

于 2018-11-09T12:45:04.347 回答
2

尽管所有答案都在那里,但让我在其中添加更多信息,可能会帮助其他用户。此处记录(官方文档):-

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

将异常处理程序安装到在主runBlocking范围内启动的协程是没有意义的,因为尽管安装了处理程序,但当其子程序以异常完成时,主协程总是会被取消。

希望这会有所帮助。

于 2019-02-10T08:35:16.960 回答
1

这可以通过稍微修改代码以使deferred值使用与范围相同CoroutineContext的方式显式执行来解决runBlocking,例如

runBlocking {
    try {
        val deferred = withContext(this.coroutineContext) {
            async {
                throw Exception()
            }
        }
        deferred.await()
    } catch (e: Exception) {
        println("Caught $e")
    }
}
println("Completed")

原始问题更新后更新

这是否提供了您想要的:

runBlocking {
    supervisorScope {
        try {
            val a = async {
                delay(1000)
                println("Done after delay")
            }
            val b = async { throw Exception() }
            awaitAll(a, b)
        } catch (e: Exception) {
            println("Caught $e")
            // Optional next line, depending on whether you want the async with the delay in it to be cancelled.
            coroutineContext.cancelChildren()
        }
    }
}

取自讨论并行分解的评论。

于 2018-11-09T08:37:00.810 回答
0

当其中一个子协程抛出异常时,正常CoroutineScope(由 创建)立即取消所有子协程。runBlocking此处记录了此行为:https ://kotlinlang.org/docs/reference/coroutines/exception-handling.html#cancellation-and-exceptions

您可以使用 asupervisorScope来获得您想要的行为。如果一个子协程在主管范围内失败,它不会立即取消其他子协程。只有在未处理异常的情况下才会取消子项。

有关更多信息,请参见此处:https ://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html

fun main() {
    runBlocking {
        supervisorScope {
            try {
                val deferred = async { throw Exception() }
                deferred.await()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
    }
    println("Completed")
}
于 2018-11-09T08:37:39.120 回答