异步方法抛出异常是否合理?
Is it reasonable to throw an exception from an asynchronous method?
在 Java 中开发具有 CompletableFuture
return 类型的异步方法,我们希望生成的 CF 正常或异常完成,具体取决于该方法是成功还是失败。
然而,考虑例如我的方法写入 AsynchronousChannel
并在打开该通道时出现异常。它甚至还没有开始写作。所以,在这种情况下,我倾向于让异常流向调用者。对吗?
尽管调用者必须处理 2 种失败情况:1) 异常,或 2) 拒绝承诺。
或者,我的方法是否应该捕获该异常并 return 拒绝承诺?
我认为两者都是有效的设计。 Datastax 实际上从第一种方法开始他们的设计,其中借用连接是阻塞的,然后切换到完全异步模型 (https://docs.datastax.com/en/developer/java-driver/3.5/upgrade_guide/#3-0-4)
作为 datastax java 驱动程序的用户,我对修复非常满意,因为它将 api 更改为真正的非阻塞(甚至打开一个通道,在您的示例中,有成本)。
不过我觉得这里没有对错之分...
从来电者的角度来看,这没有太大区别。在任何一种情况下,异常的原因都是可见的,无论它是从方法中抛出的,还是从可完成的未来调用 get() 中抛出的。
我可能会争辩说,可完成的未来抛出的异常应该是异步计算的异常,而不是未能启动该计算。
IMO,选项 1) 使 API 更难使用,因为将有两种不同的通信错误路径:
- "Synchronous" 异常,方法在抛出异常时结束。
- "Asynchronous" 异常,其中方法 returns 一个 CF,它以异常完成。请注意,不可能避免这种情况,因为总是会出现只有在异步路径启动后才发现错误的情况(例如超时)。
程序员现在必须确保正确处理这两条路径,而不是只处理一条。
同样有趣的是,C# 和 Javascript 的行为总是通过返回的 Task
/[ 报告 async
函数体内抛出的异常。 =14=],即使是在第一个 await
之前抛出的异常,也永远不会以异常结束 async
函数调用。
Kotlin 的协程也是如此,即使使用 Unconfined
调度程序
class SynchronousExceptionExamples {
@Test
fun example() {
log.info("before launch")
val job = GlobalScope.launch(Dispatchers.Unconfined) {
log.info("before throw")
throw Exception("an-error")
}
log.info("after launch")
Thread.sleep(1000)
assertTrue(job.isCancelled)
}
}
会产生
6 [main] INFO SynchronousExceptionExamples - before launch
73 [main @coroutine#1] INFO SynchronousExceptionExamples - before throw
(...)
90 [main] INFO SynchronousExceptionExamples - after launch
请注意,异常发生在 main
线程中,但是 launch
以正确的 Job
结尾。
在 Java 中开发具有 CompletableFuture
return 类型的异步方法,我们希望生成的 CF 正常或异常完成,具体取决于该方法是成功还是失败。
然而,考虑例如我的方法写入 AsynchronousChannel
并在打开该通道时出现异常。它甚至还没有开始写作。所以,在这种情况下,我倾向于让异常流向调用者。对吗?
尽管调用者必须处理 2 种失败情况:1) 异常,或 2) 拒绝承诺。
或者,我的方法是否应该捕获该异常并 return 拒绝承诺?
我认为两者都是有效的设计。 Datastax 实际上从第一种方法开始他们的设计,其中借用连接是阻塞的,然后切换到完全异步模型 (https://docs.datastax.com/en/developer/java-driver/3.5/upgrade_guide/#3-0-4)
作为 datastax java 驱动程序的用户,我对修复非常满意,因为它将 api 更改为真正的非阻塞(甚至打开一个通道,在您的示例中,有成本)。
不过我觉得这里没有对错之分...
从来电者的角度来看,这没有太大区别。在任何一种情况下,异常的原因都是可见的,无论它是从方法中抛出的,还是从可完成的未来调用 get() 中抛出的。
我可能会争辩说,可完成的未来抛出的异常应该是异步计算的异常,而不是未能启动该计算。
IMO,选项 1) 使 API 更难使用,因为将有两种不同的通信错误路径:
- "Synchronous" 异常,方法在抛出异常时结束。
- "Asynchronous" 异常,其中方法 returns 一个 CF,它以异常完成。请注意,不可能避免这种情况,因为总是会出现只有在异步路径启动后才发现错误的情况(例如超时)。
程序员现在必须确保正确处理这两条路径,而不是只处理一条。
同样有趣的是,C# 和 Javascript 的行为总是通过返回的 Task
/[ 报告 async
函数体内抛出的异常。 =14=],即使是在第一个 await
之前抛出的异常,也永远不会以异常结束 async
函数调用。
Kotlin 的协程也是如此,即使使用 Unconfined
调度程序
class SynchronousExceptionExamples {
@Test
fun example() {
log.info("before launch")
val job = GlobalScope.launch(Dispatchers.Unconfined) {
log.info("before throw")
throw Exception("an-error")
}
log.info("after launch")
Thread.sleep(1000)
assertTrue(job.isCancelled)
}
}
会产生
6 [main] INFO SynchronousExceptionExamples - before launch
73 [main @coroutine#1] INFO SynchronousExceptionExamples - before throw
(...)
90 [main] INFO SynchronousExceptionExamples - after launch
请注意,异常发生在 main
线程中,但是 launch
以正确的 Job
结尾。