解释 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,直到您真正从非协程范围启动协程?)

我使用了以下版本:

协程使用所谓的结构化并发来支持取消、异常处理等。它们被结构化为作业树,因此通常,当您创建新协程时,它们会成为当前协程的 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() 块,所以不鼓励,可能会导致上述后果。