异常传播如何在CoroutineScope e.async上工作? [英] How does exception propagation works on CoroutineScope.async?
问题描述
我看到多个来源声称发生在Async{}块内的异常不会被传递到任何地方,而只存储在Deferred
实例中。主张的是,例外仍然是隐藏的,并且只影响人们将调用await()
的时刻的外部事物。这通常被描述为launch{}
和async{}
之间的主要区别之一。Here is an example。
异步代码内部的未捕获异常存储在 结果被推迟,并且不在其他地方交付,它将获得 静默丢弃,除非已处理
根据这个声明,至少在我的理解中,下面的代码不应该抛出,因为没有人在调用等待:
// 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") }
}
所以我的问题是,为什么传递任何作业都不会引发异常,但传递作业或主管作业都不会引发异常?
推荐答案
在某种意义上,您所经历的混乱是kotlin协程在变得稳定之前就已经取得早期成功的结果。在他们实验的日子里,他们缺乏的一件事是结构化并发,在那个状态下有大量的网络材料写下了他们(比如你2017年的link 1)。一些当时有效的先入为主的观念在人们成熟后仍然存在,甚至在最近的帖子中得到了延续。
实际情况非常清楚--您所要理解的就是协程层次结构,它是通过Job
对象进行中介的。无论它是launch
、async
还是任何其他协程构建器--它们的行为都是统一的。
记住这一点,让我们来看看您的示例:
runBlocking {
async { throw Exception("Oops") }
}
通过编写async
,您隐含地使用了this.async
,其中this
是已建立的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
,但随后发生了一些意想不到的事情:没有完成,程序永远挂起。为什么?
这可能是该机制中最棘手的部分,但一旦您仔细考虑,它仍然是完全有意义的。当您创建协程时,它会在内部创建自己的Job
实例--无论您是否将作业作为参数显式提供给async
,这种情况总是会发生。如果您提供显式作业,它将成为该内部创建的作业的父级。
runBlocking
协同例程完成时,它会自动完成。但完成不会像取消那样传播到父级-您不会希望因为一个子协程正常完成而停止所有操作。
因此,当您创建自己的Job
实例并将其作为async
协程的父级提供时,您的工作不会通过任何方式完成。如果协程失败,则失败会传播到您的作业,但如果它正常完成,您的作业将永远保持正在进行中的原始状态。
最后,让我们再次引入SupervisorJob
:
runBlocking {
async(SupervisorJob(coroutineContext[Job])) {
delay(1000)
throw Exception("Oops")
}
}
这将永远运行,不会有任何输出,因为SupervisorJob
会接受该异常。
这篇关于异常传播如何在CoroutineScope e.async上工作?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!