为什么这会导致 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
(和一些等待的条件,所以我们不退出程序)。
如果在主例程的循环中启用日志记录,您会看到死锁发生在第三次迭代时总是。其他例程的执行次数取决于调度程序。回顾一下这个故事:第一次迭代是第一个球的开球;下一次迭代是一个神秘的第二球,但这可以处理。第三次迭代是一个僵局——它使第三个球复活,任何人都无法处理。
这不是关于如何更好地编写它的问题。这是一个专门关于为什么 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
(和一些等待的条件,所以我们不退出程序)。
如果在主例程的循环中启用日志记录,您会看到死锁发生在第三次迭代时总是。其他例程的执行次数取决于调度程序。回顾一下这个故事:第一次迭代是第一个球的开球;下一次迭代是一个神秘的第二球,但这可以处理。第三次迭代是一个僵局——它使第三个球复活,任何人都无法处理。