为什么这会在Go中引发死锁?

huangapple go评论94阅读模式
英文:

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-pong”(而不仅仅是一次)?

英文:

This is not a question about how to better write this. It's a question specifically about why Go is causing a deadlock in this scenario.

package main

import &quot;fmt&quot;

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

    go func() {
        for {
            &lt;-chan1
            fmt.Printf(&quot;chan1\n&quot;)
            chan2 &lt;- true
        }
    }()

    go func() {
        for {
            &lt;-chan2
            fmt.Printf(&quot;chan2\n&quot;)
            chan1 &lt;- true
        }
    }()

    for {
        chan1 &lt;- true
    }
}

Outputs:

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

Why does this not cause an infinite loop? How come it does two full "ping-pings" (instead of just one) before giving up?

答案1

得分: 8

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

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

你有两个玩家(例程)和一个球(true 值)。每个玩家都在等待一个球,一旦他们得到球,他们就会把球传递给另一个玩家(通过一个通道)。这就是你的两个例程实际上在做的事情,这确实会产生一个无限循环。

问题出在你的主循环中引入的第三个玩家。他躲在第二个玩家后面,一旦他看到第一个玩家手里空空如也,他就向他扔出另一个球。所以我们最终导致两个玩家都拿着一个球,无法将球传递给另一个玩家,因为另一个玩家已经拿着(第一个)球。这个隐藏的邪恶玩家还试图传递另一个球。每个人都很困惑,因为有三个球,三个玩家,没有空手。

换句话说,你引入了破坏游戏的第三个玩家。他应该是一个仲裁者,在游戏开始时传递第一个球,并观察它,但停止产生球!这意味着,你的主例程中不应该有一个循环,而应该只有 chan1 <- true(还有一些条件来等待,以便我们不退出程序)。

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

英文:

From the runtime perspective you get a deadlock because all routines try to send onto a channel and there's no routine waiting to receive anything.

But why is it happening? I will give you a story as I like visualising what my routines are doing when I encounter a deadlock.

You have two players (routines) and one ball (true value). Every player waits for a ball and once they get it they pass it back to the other player (through a channel). This is what your two routines are really doing and this would indeed produce an infinite loop.

The problem is the third player introduced in your main loop. He's hiding behind the second player and once he sees the first player has empty hands, he throws another ball at him. So we end up with both players holding a ball, couldn't pass it to another player because the other one has (the first) ball in his hands already. The hidden, evil player is also trying to pass yet another ball. Everyone is confused, because there're three balls, three players and no empty hands.

In other words, you have introduced the third player who is breaking the game. He should be an arbiter passing the very first ball at the beginning of the game, watching it, but stop producing balls! It means, instead of having a loop in you main routine, there should be simply chan1 &lt;- true (and some condition to wait, so we don't exit the program).

If you enable logging in the loop of main routine, you will see the deadlock occurs always on the third iteration. The number of times the other routines are executed depends on the scheduler. Bringing back the story: first iteration is a kick-off of the first ball; next iteration is a mysterious second ball, but this can be handled. The third iteration is a deadlock – it brings to life the third ball which can't be handled by anybody.

答案2

得分: 1

这里告诉了你一切:所有的goroutine都被阻塞在试图发送到一个没有接收者的通道上。

所以你的第一个goroutine在 chan2 <- true 上被阻塞,第二个在 chan1 <- true 上被阻塞,而主goroutine在自己的 chan1 <- true 上被阻塞。

至于为什么它会像你说的那样进行两次“完整的ping-pong”,这取决于调度和 <-chan1 决定先接收哪个发送者。

在我的电脑上,我得到了更多的输出,并且每次运行时都会有所变化:

chan1
chan2
chan1
chan2
chan1
chan2
chan1
chan2
chan1
chan2
chan1
chan2
chan1
fatal error: all goroutines are asleep - deadlock!
英文:
goroutine 1 [chan send]:
goroutine 5 [chan send]:
goroutine 6 [chan send]:

This tells it all: all your goroutines are blocked trying to send on a channel with no one to receive on the other end.

So your first goroutine blocks on chan2 &lt;- true, your second blocks on chan1 &lt;- true and your main goroutine blocks on its own chan1 &lt;- true.

As to why it does two "full ping-pings" like you say, it depends on scheduling and from which sender &lt;-chan1 decides to receive first.

On my computer, I get more and it varies each time I run it:

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

答案3

得分: 1

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

当出现以下情况时,会发生死锁:

  • 第一个例程试图写入 chan2
  • 第二个例程试图写入 chan1
  • 主程序试图写入 chan1

这是如何发生的?举个例子:

  • 主程序写入 chan1,在另一个写操作上阻塞
  • 例程1:从主程序接收 chan1,打印,然后在写入 chan2 上阻塞
  • 例程2:接收 chan2,打印,然后在写入 chan1 上阻塞
  • 例程1:从例程2接收 chan1,打印,然后在写入 chan2 上阻塞
  • 例程2:接收 chan2,打印,然后在写入 chan1 上阻塞
  • 主程序写入 chan1,在另一个写操作上阻塞
  • 例程1:从主程序接收 chan1,打印,然后在写入 chan2 上阻塞
  • 主程序写入 chan1,在另一个写操作上阻塞

目前所有的例程都被阻塞了,也就是说:

例程1无法写入 chan2,因为例程2没有在接收,而是被阻塞在试图写入 chan1 上。但是没有人在监听 chan1

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

英文:

It looks complicated but the answer is easy.

It'll deadlock when:

  • First routine is trying to write to chan2
  • Second route is trying to write to chan1.
  • Main is trying to write to chan1.

How can that happen? Example:

  • Main writes chan1. Blocks on another write.
  • Routine 1: chan1 receives from Main. Prints. Blocks on write chan2.
  • Routine 2: chan2 receives. Prints. Blocks on write chan1.
  • Routine 1: chan1 receives from Routine 2. Prints. Blocks on write chan2.
  • Routine 2: chan2 receives. Prints. Blocks on write chan1.
  • Main writes chan1. Blocks on another write.
  • Routine 1: chan1 receives from Main. Prints. Blocks on write chan2.
  • Main writes chan1. Blocks on another write.

Currently all routines are blocked. i.e.:

Routine 1 cannot write to chan2 because Routine 2 is not receiving but is actually blocked trying to write to chan1. But no one is listening on chan1.

As @HectorJ said, it all depends on the scheduler. But in this setup, a deadlock is inevitable.

huangapple
  • 本文由 发表于 2015年11月17日 20:35:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/33757107.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定