在试验kotlin协同实验时,我遇到了一种情况,即出现死锁,这是我所没有想到的。
我将代码简化为以下显示问题的最小代码示例:
@Test
fun deadlockTest() {
runBlocking {
val job = launch {
runBlocking {
println("await cancellation")
awaitCancellation()
}
}
println("launched job")
delay(100)
println("waited a bit")
job.cancelAndJoin()
println("canceled and joined")
}
assertTrue(true)
}结果是
launched job
await cancellation
waited a bit它从未超越过job.cancelAndJoin,就好像出现了一些死锁。
如果我将代码稍微更改为以下内容:
@Test
fun fixedDeadlockTest() {
runBlocking {
val job = launch {
withContext(Dispatchers.Default) { // <-- this is the only difference
println("awaiting cancellation")
awaitCancellation()
}
}
println("launched job")
delay(100)
println("waited a bit")
job.cancelAndJoin()
println("canceled and joined")
}
assertTrue(true)
}一切正常,所有的行都被打印出来,测试完成。
问题是:为什么这段代码会导致死锁,将runBlocking放入另一个runBlocking的启动中是一种不好的做法吗?(也就是说,在您的代码中永远不要使用runBlocking,直到您真正从非协同范围启动协同线吗?)
我使用了以下版本:
多平台1.4.32
发布于 2021-07-27 10:00:32
Coroutines使用所谓的结构化并发来支持取消、异常处理等。它们被构造成一棵作业树,所以通常,当您创建新的coroutine时,它们就会成为当前协同线的子级。父母和子女之间有具体的责任,例如取消父母取消其所有子女。
然而,有一些方法可以启动不与当前协同线相连的协同线。例如,当您提供特定的CoroutineScope或使用runBlocking()时,就会发生这种情况。请注意,与launch()或async()相反,runBlocking()不需要从协同线运行。它主要用于桥接非协同线和协同线代码,因此它从“根”开始创建协同线--它们与其他coroutine分离。
由于上述原因,您的示例中的cancelAndJoin()取消了在launch()中运行的协同线,但它不会取消在runBlocking()中运行的协同线。"awaitCancellation“协同线与”启动“协同线分离,因此它忽略了它的取消。
下面是我最初的答案。我对这一僵局的主要原因是错误的,但我所说的基本上仍然是正确的,它补充了上面的答案,所以我保留原样。我之所以错了,是因为我忘记了runBlocking()内部使用线程局部变量来存储它的事件循环。这意味着运行在另一个runBlocking()中的runBlocking()实际上共享相同的事件循环/调度程序,因此通过在awaitCancellation()挂起它就可以从delay()恢复。不过,我认为这样做是不可取的。
原始答案:
死锁的发生是因为外部runBlocking()启动了一个单线程协同调度器来在其中启动协同器,而内部runBlocking()阻塞了这个单一和唯一的线程。
您是对的,runBlocking()主要用于桥接非协同线和协同线代码.没有硬性规定在协同线内禁止使用runBlocking(),但一般情况下,我们应该避免阻塞内部协同线,而应该暂停。runBlocking()阻塞,因此它是不鼓励的,并可能导致像上面这样的后果。
https://stackoverflow.com/questions/68542554
复制相似问题