此代码:
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 forum上的线程改编了这个问题。
如果我们不想在一个协程失败时取消所有协程,则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")
}
现在输出为
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
这又不是我想要的行为。在这里,launch
协同程序失败,但未处理的异常使其他协同程序的工作无效,但它们继续进行不间断。
我认为合理的行为是当协程以无法预见的方式(即未处理的)失败时散布取消指令。从await
捕获异常意味着没有任何全局错误,只有作为业务逻辑一部分处理的本地化异常。
答案 0 :(得分:3)
研究了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)
但这很脆弱,因为您必须当心以确保对每个可挂起的呼叫都执行此操作。这也违反了科特林的“默认顺序”原则。
总而言之:当前的设计虽然起初是违反直觉的,但确实是可行的解决方案。除非您正在并行分解任务,否则它还会增强不使用async-await
的规则。
答案 1 :(得分: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()
}
}
}
这摘自this评论,该评论讨论了并行分解。
答案 2 :(得分:1)
尽管所有答案都就在那儿,但让我在其中提供更多启示,可能会对其他用户有所帮助。 here(Official doc)记录为:-
如果协程遇到
CancellationException
以外的异常, 它会取消其父项,但有例外。此行为不能是 覆盖并用于为以下对象提供稳定的协程层次结构 structured concurrency不依赖 CoroutineExceptionHandler实现。最初的例外是 当所有子项终止时,由父项(在GlobalScope中)处理。在协程中安装异常处理程序没有任何意义 在主runBlocking的范围内启动,因为主 当它的孩子完成时,协程将总是被取消 尽管已安装处理程序,但还是有例外。
希望这会有所帮助。
答案 3 :(得分:0)
普通CoroutineScope
(由runBlocking
创建)在其中一个子协程抛出异常时立即取消所有子协程。此行为记录在这里:https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#cancellation-and-exceptions
您可以使用supervisorScope
来获得所需的行为。如果子协程在主管范围内失败,则不会立即取消其他子协程。仅当未处理异常时,子级才会被取消。
有关更多信息,请参见此处: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")
}