为什么这会导致 Go 中的死锁?

Why does this cause a deadlock in Go?

这不是关于如何更好地编写它的问题。这是一个专门关于为什么 Go 在这种情况下会导致死锁的问题。

package main

import "fmt"

func main() {
    chan1 := make(chan bool)
    chan2 := make(chan bool)

    go func() {
        for {
            <-chan1
            fmt.Printf("chan1\n")
            chan2 <- true
        }
    }()

    go func() {
        for {
            <-chan2
            fmt.Printf("chan2\n")
            chan1 <- true
        }
    }()

    for {
        chan1 <- true
    }
}

输出:

chan1
chan2
chan1
chan2
chan1
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
goroutine 5 [chan send]:
goroutine 6 [chan send]:
exit status 2

为什么这不会导致无限循环?为什么它在放弃之前做了两个完整的 "ping-pings"(而不是一个)?

goroutine 1 [chan send]:
goroutine 5 [chan send]:
goroutine 6 [chan send]:

这说明了一切:您所有的 goroutine 都被阻止尝试在另一端没有人接收的通道上发送。

因此,您的第一个 goroutine 在 chan2 <- true 上阻塞,您的第二个 goroutine 在 chan1 <- true 上阻塞,而您的主要 goroutine 在 chan1 <- true.

上阻塞

至于为什么它会像您说的那样执行两次 "full ping-pings",这取决于调度以及发件人 <-chan1 决定首先接收的信息。

在我的电脑上,我得到了更多,而且每次我 运行 它都不同:

chan1
chan2
chan1
chan2
chan1
chan2
chan1
chan2
chan1
chan2
chan1
chan2
chan1
fatal error: all goroutines are asleep - deadlock!

看起来很复杂,但答案很简单。

它会在以下情况发生死锁:

  • 第一个例程正在尝试写入 chan2
  • 第二条路线正在尝试写入 chan1
  • Main 正在尝试写入 chan1

怎么会这样?示例:

  • 主要写chan1。另一个写入块。
  • 例程 1:chan1 从 Main 接收。印刷。写入块 chan2.
  • 例程 2:chan2 接收。印刷。写入块 chan1.
  • 例程 1:chan1 从例程 2 接收。打印。写入块 chan2.
  • 例程 2:chan2 接收。印刷。写入块 chan1.
  • 主要写chan1。另一个写入块。
  • 例程 1:chan1 从 Main 接收。印刷。写入块 chan2.
  • 主要写chan1。另一个写入块。

目前所有例程都被屏蔽了。即:

例程 1 无法写入 chan2,因为例程 2 未接收但实际上在尝试写入 chan1 时被阻止。但是 chan1.

上没有人在听

正如@HectorJ 所说,这完全取决于调度程序。但是在这个设置中,死锁是不可避免的。

从运行时的角度来看,您会遇到死锁,因为所有例程都试图发送到通道上,但没有例程在等待接收任何东西。

但是为什么会发生这种情况?我会给你一个故事,因为我喜欢想象遇到死锁时我的例程在做什么。

你有两个球员(套路)和一个球(true值)。每个玩家都等待一个球,一旦他们拿到球,他们就会将球传回给另一个玩家(通过通道)。这就是你的两个例程真正在做的事情,这确实会产生无限循环。

问题出在您的主循环中引入的第三个播放器。他躲在第二名球员身后,一旦他看到第一名球员两手空空,他就会向他扔 另一个 球。所以我们最终两个球员都拿着球,不能把球传给另一个球员,因为另一个球员已经拿到了(第一个)球。隐藏的邪恶玩家还试图传 另一个 球。大家都很困惑,因为三个球,三个球员,没有空手。

也就是说,你介绍了第三个打破比赛的选手。他应该是比赛开始时传出第一个球的仲裁者,看着它,但停止生产球!这意味着,而不是在你的主例程中有一个循环,应该有简单的 chan1 <- true (和一些等待的条件,所以我们不退出程序)。

如果在主例程的循环中启用日志记录,您会看到死锁发生在第三次迭代时总是。其他例程的执行次数取决于调度程序。回顾一下这个故事:第一次迭代是第一个球的开球;下一次迭代是一个神秘的第二球,但这可以处理。第三次迭代是一个僵局——它使第三个球复活,任何人都无法处理。