异常传播如何在 CoroutineScope.async 上起作用?
How does exception propagation works on CoroutineScope.async?
我看到多个消息来源声称 async{} 块内发生的异常不会传递到任何地方,并且仅存储在 Deferred
实例中。声称异常仍然是“隐藏的”,并且只会在调用 await()
的那一刻影响外部的事物。这通常被描述为 launch{}
和 async{}
之间的主要区别之一。 .
An uncaught exception inside the async code is stored inside the
resulting Deferred and is not delivered anywhere else, it will get
silently dropped unless processed
根据这个说法,至少按照我的理解,下面的代码应该不会抛出,因为没有人在调用 await:
// throws
runBlocking {
async { throw Exception("Oops") }
}
然而,异常被抛出。这也被讨论here,但我无法真正理解为什么阅读这个。
所以在我看来,当异步抛出时,“取消信号”会在父作用域上传播,即使 await()
没有被调用。正如上面引述的那样,也就是异常并没有真正保持隐藏,也没有悄无声息地丢弃。我的假设正确吗?
现在,如果我们传递 SupervisorJob()
,代码 不会 抛出:
// does not throw
runBlocking {
async(SupervisorJob()) { throw Exception("Oops") }
}
这似乎是合理的,因为主管的工作是为了吞下失败。
现在是我完全不明白的部分。如果我们传递 Job()
,即使 Job()
应该将失败传播到其父范围,代码仍会运行而不会抛出:
// does not throw. Why?
runBlocking {
async(Job()) { throw Exception("Oops") }
}
所以我的问题是,为什么传递 Job 没有抛出,但传递 Job 或 SupervisorJob 都没有抛出?
从某种意义上说,您遇到的混乱是 Kotlin 协程在变得稳定之前取得早期成功的结果。在他们的实验日,他们缺乏的一件事是结构化并发,并且在该状态下有大量网络 material 写了关于他们的文章(例如你的 从 2017 年开始)。一些 then-valid 成见甚至在人们成熟之后仍然存在,并在最近的帖子中得到延续。
实际情况很清楚——你只需要了解协程层次结构,它是通过 Job
对象进行调解的。它是 launch
还是 async
或任何其他协程构建器并不重要 — 它们的行为都是一致的。
考虑到这一点,让我们看一下您的示例:
runBlocking {
async { throw Exception("Oops") }
}
通过只写 async
,您隐含地使用了 this.async
,其中 this
是 runBlocking
建立的 CoroutineScope
。它包含与 runBlocking
协程关联的 Job
实例。由于这个原因,async
协程成为runBlocking
的子协程,所以后者在async
协程失败时抛出异常。
runBlocking {
async(SupervisorJob()) { throw Exception("Oops") }
}
此处您提供了一个没有父项的独立作业实例。这打破了协程层次结构并且 runBlocking
不会失败。事实上,runBlocking
甚至不会等待协程完成——添加一个 delay(1000)
来验证这一点。
runBlocking {
async(Job()) { throw Exception("Oops") }
}
这里没有新的推理 — Job
或 SupervisorJob
,没关系。您破坏了协程层次结构并且失败不会传播。
现在让我们探索更多变体:
runBlocking {
async(Job(coroutineContext[Job])) {
delay(1000)
throw Exception("Oops")
}
}
现在我们创建了一个新的 Job
实例,但我们将其设为 runBlocking
的子实例。这会引发异常。
runBlocking {
async(Job(coroutineContext[Job])) {
delay(1000)
println("Coroutine done")
}
}
同上,但现在我们不抛出异常,async
协程正常完成。它打印 Coroutine done
,但随后发生了意想不到的事情:runBlocking
未 完成,程序永远挂起。为什么?
这可能是此机制中最棘手的部分,但一旦您仔细考虑,它仍然非常有意义。当您创建协程时,它会在内部创建自己的 Job
实例——这种情况总是会发生,无论您是否明确提供作业作为 async
的参数。如果您提供明确的工作,它将成为该 internally-created 工作的 parent。
现在,在第一种情况下,您没有提供明确的工作,父工作是由 runBlocking
在内部创建的。它会在 runBlocking
协程完成时自动完成。但是完成不会像取消那样传播到父级 — 你不希望一切都因为一个子协程正常完成而停止。
因此,当您创建自己的 Job
实例并将其作为 async
协程的父级提供时,您的工作将无法完成。如果协同程序失败,失败会传播到您的作业,但如果它正常完成,您的作业将永远保持“进行中”的原始状态。
最后,让我们再次引入 SupervisorJob
:
runBlocking {
async(SupervisorJob(coroutineContext[Job])) {
delay(1000)
throw Exception("Oops")
}
}
这将永远运行而没有任何输出,因为 SupervisorJob
吞下了异常。
我看到多个消息来源声称 async{} 块内发生的异常不会传递到任何地方,并且仅存储在 Deferred
实例中。声称异常仍然是“隐藏的”,并且只会在调用 await()
的那一刻影响外部的事物。这通常被描述为 launch{}
和 async{}
之间的主要区别之一。
An uncaught exception inside the async code is stored inside the resulting Deferred and is not delivered anywhere else, it will get silently dropped unless processed
根据这个说法,至少按照我的理解,下面的代码应该不会抛出,因为没有人在调用 await:
// throws
runBlocking {
async { throw Exception("Oops") }
}
然而,异常被抛出。这也被讨论here,但我无法真正理解为什么阅读这个。
所以在我看来,当异步抛出时,“取消信号”会在父作用域上传播,即使 await()
没有被调用。正如上面引述的那样,也就是异常并没有真正保持隐藏,也没有悄无声息地丢弃。我的假设正确吗?
现在,如果我们传递 SupervisorJob()
,代码 不会 抛出:
// does not throw
runBlocking {
async(SupervisorJob()) { throw Exception("Oops") }
}
这似乎是合理的,因为主管的工作是为了吞下失败。
现在是我完全不明白的部分。如果我们传递 Job()
,即使 Job()
应该将失败传播到其父范围,代码仍会运行而不会抛出:
// does not throw. Why?
runBlocking {
async(Job()) { throw Exception("Oops") }
}
所以我的问题是,为什么传递 Job 没有抛出,但传递 Job 或 SupervisorJob 都没有抛出?
从某种意义上说,您遇到的混乱是 Kotlin 协程在变得稳定之前取得早期成功的结果。在他们的实验日,他们缺乏的一件事是结构化并发,并且在该状态下有大量网络 material 写了关于他们的文章(例如你的
实际情况很清楚——你只需要了解协程层次结构,它是通过 Job
对象进行调解的。它是 launch
还是 async
或任何其他协程构建器并不重要 — 它们的行为都是一致的。
考虑到这一点,让我们看一下您的示例:
runBlocking {
async { throw Exception("Oops") }
}
通过只写 async
,您隐含地使用了 this.async
,其中 this
是 runBlocking
建立的 CoroutineScope
。它包含与 runBlocking
协程关联的 Job
实例。由于这个原因,async
协程成为runBlocking
的子协程,所以后者在async
协程失败时抛出异常。
runBlocking {
async(SupervisorJob()) { throw Exception("Oops") }
}
此处您提供了一个没有父项的独立作业实例。这打破了协程层次结构并且 runBlocking
不会失败。事实上,runBlocking
甚至不会等待协程完成——添加一个 delay(1000)
来验证这一点。
runBlocking {
async(Job()) { throw Exception("Oops") }
}
这里没有新的推理 — Job
或 SupervisorJob
,没关系。您破坏了协程层次结构并且失败不会传播。
现在让我们探索更多变体:
runBlocking {
async(Job(coroutineContext[Job])) {
delay(1000)
throw Exception("Oops")
}
}
现在我们创建了一个新的 Job
实例,但我们将其设为 runBlocking
的子实例。这会引发异常。
runBlocking {
async(Job(coroutineContext[Job])) {
delay(1000)
println("Coroutine done")
}
}
同上,但现在我们不抛出异常,async
协程正常完成。它打印 Coroutine done
,但随后发生了意想不到的事情:runBlocking
未 完成,程序永远挂起。为什么?
这可能是此机制中最棘手的部分,但一旦您仔细考虑,它仍然非常有意义。当您创建协程时,它会在内部创建自己的 Job
实例——这种情况总是会发生,无论您是否明确提供作业作为 async
的参数。如果您提供明确的工作,它将成为该 internally-created 工作的 parent。
现在,在第一种情况下,您没有提供明确的工作,父工作是由 runBlocking
在内部创建的。它会在 runBlocking
协程完成时自动完成。但是完成不会像取消那样传播到父级 — 你不希望一切都因为一个子协程正常完成而停止。
因此,当您创建自己的 Job
实例并将其作为 async
协程的父级提供时,您的工作将无法完成。如果协同程序失败,失败会传播到您的作业,但如果它正常完成,您的作业将永远保持“进行中”的原始状态。
最后,让我们再次引入 SupervisorJob
:
runBlocking {
async(SupervisorJob(coroutineContext[Job])) {
delay(1000)
throw Exception("Oops")
}
}
这将永远运行而没有任何输出,因为 SupervisorJob
吞下了异常。