CoroutineScope.async 上的异常传播如何工作?

Alk*_*dis 8 async-await kotlin kotlin-coroutines

我看到多个消息来源声称 async{} 块内发生的异常不会传递到任何地方,仅存储在实例中Deferred。据称,该异常仍然是“隐藏的”,并且仅在调用时影响外部事物await()launch{}这通常被描述为和之间的主要区别之一async{}这是一个例子

异步代码中未捕获的异常存储在生成的 Deferred 中,并且不会传递到其他任何地方,除非进行处理,否则它将被静默丢弃

根据这一说法,至少按照我的理解,以下代码不应该抛出异常,因为没有人调用await:

// throws
runBlocking {
  async { throw Exception("Oops") }
}
Run Code Online (Sandbox Code Playgroud)

然而,异常还是被抛出了。这里也讨论了这一点,但通过阅读本文我无法真正理解为什么。

所以在我看来,当异步抛出时,即使await()没有被调用,也会在父作用域上传播“取消信号”。也就是说,异常并没有真正隐藏起来,也没有默默地被丢弃,正如上面引用的那样。我的假设正确吗?

现在,如果我们传递 a SupervisorJob(),代码不会抛出

// does not throw
runBlocking {
  async(SupervisorJob()) { throw Exception("Oops") }
}
Run Code Online (Sandbox Code Playgroud)

这似乎是合理的,因为主管的工作就是要承受失败。

现在是我完全不明白的部分。如果我们传递Job(),代码仍然会运行而不会抛出异常,即使Job()应该将失败传播到其父作用域:

// does not throw. Why?
runBlocking {
  async(Job()) { throw Exception("Oops") }
}
Run Code Online (Sandbox Code Playgroud)

所以我的问题是,为什么不传递 Job 会抛出异常,但传递 Job 或 SupervisorJob 不会抛出异常?

Mar*_*nik 12

从某种意义上说,您所经历的混乱是 Kotlin 协程在稳定之前就取得了早期成功的结果。在他们的实验日子里,他们缺乏的一件事是结构化并发,并且在这种状态下写了大量关于它们的网络材料(例如2017 年的链接 1)。一些当时有效的先入之见即使在人们成熟之后仍然存在,并且在最近的帖子中得到延续。

\n

实际情况很清楚 \xe2\x80\x94 你所需要理解的就是协程层次结构,它是通过对象来中介的Job。无论是 salaunch还是 an async,或者任何进一步的协程构建器 \xe2\x80\x94 ,它们的行为都是一致的。

\n

考虑到这一点,让我们看一下您的示例:

\n
runBlocking {\n  async { throw Exception("Oops") }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

通过只写async你隐式使用的this.async,哪里是this建立的。它包含与协程关联的实例。因此,协程成为 的子级,因此当协程失败时后者会抛出异常。CoroutineScoperunBlockingJobrunBlockingasyncrunBlockingasync

\n
runBlocking {\n  async(SupervisorJob()) { throw Exception("Oops") }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,您提供一个没有父级的独立作业实例。这会破坏协程层次结构并且runBlocking不会失败。事实上,runBlocking甚至不需要等待你的协程完成 \xe2\x80\x94 添加 adelay(1000)来验证这一点。

\n
runBlocking {\n  async(Job()) { throw Exception("Oops") }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这里没有新的推理 \xe2\x80\x94JobSupervisorJob,这并不重要。您破坏了协程层次结构,并且故障不会传播。

\n

现在让我们探索更多的变化:

\n
runBlocking {\n    async(Job(coroutineContext[Job])) {\n        delay(1000)\n        throw Exception("Oops")\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们创建了一个新Job实例,但我们将其作为 的子实例runBlocking。这会引发异常。

\n
runBlocking {\n    async(Job(coroutineContext[Job])) {\n        delay(1000)\n        println("Coroutine done")\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

与上面相同,但现在我们不会抛出异常并且async协程正常完成。它打印Coroutine done,但随后发生了一些意想不到的事情:runBlocking没有完成,并且程序永远挂起。为什么?

\n

这可能是这个机制中最棘手的部分,但一旦你仔细考虑一下,它仍然很有意义。当您创建协程时,它会在内部创建自己的Job实例 \xe2\x80\x94,无论您是否明确提供作业作为参数,这种情况总是会发生async。如果您提供显式作业,它将成为该内部创建的作业的父作业。

\n

现在,在第一种情况下,您没有提供显式作业,父作业是由 内部创建的作业runBlocking。当协程完成时它会自动完成runBlocking。但是完成不会像取消那样传播到父协程 \xe2\x80\x94 你不会希望一切都停止只是因为一个子协程正常完成。

\n

因此,当您创建自己的Job实例并将其作为协程的父级提供时async,您的工作并没有通过任何操作完成。如果协程失败,则失败会传播到您的作业,但如果正常完成,您的作业将永远保持在“正在进行”的原始状态。

\n

最后,让我们引入SupervisorJob再次引入:

\n
runBlocking {\n    async(SupervisorJob(coroutineContext[Job])) {\n        delay(1000)\n        throw Exception("Oops")\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这会永远运行而没有任何输出,因为SupervisorJob吞掉了异常。

\n