解释 kotlin 协程中死锁的原因
Explain reason for deadlock in kotlin coroutines
在尝试使用 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,直到您真正从非协程范围启动协程?)
我使用了以下版本:
- kotlin 多平台 1.4.32
- kotlinx-coroutines-core 版本 1.5.0-native-mt
协程使用所谓的结构化并发来支持取消、异常处理等。它们被结构化为作业树,因此通常,当您创建新协程时,它们会成为当前协程的 children。 parent 和 children 之间有特定的职责,例如取消 parent 会取消其所有 children.
但是,有一些方法可以启动不附加到当前协程的协程。这发生在例如如果您提供特定的 CoroutineScope
或者如果您使用 runBlocking()
。请注意,与 launch()
或 async()
相反,runBlocking()
不需要来自协程的 运行。它主要设计用于桥接 non-coroutine 和协程代码,因此它从“根”开始创建协程 - 它们与其他协程分离。
基于以上原因,你的例子中的cancelAndJoin()
取消了launch()
里面的协程运行ning,但是没有取消[=11]里面的协程运行ning =]. “awaitCancellation”协程与“launch”协程分离,因此它会忽略它的取消。
以下是我的原回答。我对这个僵局的主要原因是错误的,但我说的大部分是正确的,它补充了上面的答案,所以我保持原样。我之所以错是因为我忘记了runBlocking()
内部使用了一个线程局部变量来存储它的事件循环。这意味着 runBlocking()
运行 在另一个 runBlocking()
中实际上共享相同的事件循环/调度程序,因此通过在 awaitCancellation()
暂停它可以从 delay()
恢复。不过,我认为不鼓励这样做。
原回答:
死锁的发生是因为外部 runBlocking()
启动了一个 single-threaded 协程调度程序来启动它内部的协程,而内部 runBlocking()
阻塞了这个唯一的线程。
你是对的,runBlocking()
主要用于桥接non-coroutine和协程代码。没有硬性规定在协程中禁止使用 runBlocking()
,但通常我们应该避免在协程中阻塞,我们应该挂起。 runBlocking()
块,所以不鼓励,可能会导致上述后果。
在尝试使用 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,直到您真正从非协程范围启动协程?)
我使用了以下版本:
- kotlin 多平台 1.4.32
- kotlinx-coroutines-core 版本 1.5.0-native-mt
协程使用所谓的结构化并发来支持取消、异常处理等。它们被结构化为作业树,因此通常,当您创建新协程时,它们会成为当前协程的 children。 parent 和 children 之间有特定的职责,例如取消 parent 会取消其所有 children.
但是,有一些方法可以启动不附加到当前协程的协程。这发生在例如如果您提供特定的 CoroutineScope
或者如果您使用 runBlocking()
。请注意,与 launch()
或 async()
相反,runBlocking()
不需要来自协程的 运行。它主要设计用于桥接 non-coroutine 和协程代码,因此它从“根”开始创建协程 - 它们与其他协程分离。
基于以上原因,你的例子中的cancelAndJoin()
取消了launch()
里面的协程运行ning,但是没有取消[=11]里面的协程运行ning =]. “awaitCancellation”协程与“launch”协程分离,因此它会忽略它的取消。
以下是我的原回答。我对这个僵局的主要原因是错误的,但我说的大部分是正确的,它补充了上面的答案,所以我保持原样。我之所以错是因为我忘记了runBlocking()
内部使用了一个线程局部变量来存储它的事件循环。这意味着 runBlocking()
运行 在另一个 runBlocking()
中实际上共享相同的事件循环/调度程序,因此通过在 awaitCancellation()
暂停它可以从 delay()
恢复。不过,我认为不鼓励这样做。
原回答:
死锁的发生是因为外部 runBlocking()
启动了一个 single-threaded 协程调度程序来启动它内部的协程,而内部 runBlocking()
阻塞了这个唯一的线程。
你是对的,runBlocking()
主要用于桥接non-coroutine和协程代码。没有硬性规定在协程中禁止使用 runBlocking()
,但通常我们应该避免在协程中阻塞,我们应该挂起。 runBlocking()
块,所以不鼓励,可能会导致上述后果。