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

时间:2018-11-09 08:17:54

标签: kotlin kotlin-coroutines

此代码:

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捕获异常意味着没有任何全局错误,只有作为业务逻辑一部分处理的本地化异常。

4 个答案:

答案 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)

尽管所有答案都就在那儿,但让我在其中提供更多启示,可能会对其他用户有所帮助。 hereOfficial 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")
}