Golang中在同一个Go协程关闭2个通道会导致死锁吗?
Golang中在同一个Go协程关闭2个通道会导致死锁吗? 我正在尝试《Go语言之旅》中的“等价二叉查找树”练习。我已经完成了代码,但注意到一些奇怪的现象:
package main
import "golang.org/x/tour/tree"
// Walk 遍历树 t,将树中的所有值发送到通道 ch。
func Walk(t *tree.Tree, ch chan<- int) {
if t == nil {
return
}
Walk(t.Left, ch)
ch <- t.Value
Walk(t.Right, ch)
}
// Same 判断树 t1 和 t2 是否包含相同的值。
func Same(t1, t2 *tree.Tree) bool {
c1 := make(chan int)
c2 := make(chan int)
go func() {
Walk(t1, c1)
close(c1)
}()
go func() {
Walk(t2, c2)
close(c2)
}()
for i := range c1 {
if i != <-c2 {
return false
}
}
return true
}
func main() {
println(Same(tree.New(10), tree.New(10)))
}
如你所见,在 Same 方法中,如果我使用下面这段等价的代码替换,就会发生死锁。有人能帮我解释一下这个问题吗?谢谢。
go func() {
Walk(t1, c1)
close(c1)
Walk(t2, c2)
close(c2)
}()
更多关于Golang中在同一个Go协程关闭2个通道会导致死锁吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
它运行在不同的 goroutine 中,Chan 本质上是一个带锁的阻塞队列。 chan 接收一个值,如果没有数据就会被阻塞;发送一个值,如果 chan 已满也会被阻塞。
谢谢。我大概明白了。但我还有一个问题:为什么“发送20”必须等待range循环将其取出?我的意思是,在我看来,它是在不同的线程中,为什么我们不能独立地向通道发送数据呢?
感觉像是有一个互斥锁或类似的东西c1。
谢谢
- 你的“walker goroutine”启动
- 它进入
Walk(t1, c1) Walk(t1, c1)向c1发送第一个值- 你的主 goroutine 在 for … range 循环中从
c1接收到第一个值 - 你的主 goroutine 尝试从
c2接收值,以便与从c1接收的值进行比较 - 你的“walker goroutine”尚未从
Walk(t1, c1)返回,因此Walk(t2, c2)尚未启动。没有值被发送到c2,所以从它接收值将会导致死锁。
main 和 go1 都阻塞了。 main 需要接收 ch1 和 ch2,但没有接收到 ch2。go1 在阻塞状态下没有向 c2 发送值,因此 main 在阻塞状态下接收 c2,所有 goroutine 都进入阻塞状态。
- go1 成功发送 c1-10
- main 成功接收 c1-10
- main 等待接收 c2
- go1 发送 c2-20 时阻塞
https://play.golang.org/p/Ji-ysAFdISI
g1 需要向 c1 发送 10 次值(10-100),然后向 c2 发送 10 次。 main 需要先接收 c1 再接收 c2,重复 10 次。 但是 g1 发送 ‘20’ 时,main 处于阻塞状态等待接收 c2,因此 g1 和 main 都阻塞了。如果 c1 和 c2 的容量是 10,则不会阻塞。
在同一个Go协程中关闭两个通道确实会导致死锁,但根本原因不是关闭操作本身,而是通道的消费模式与生产模式不匹配。
在你的原始代码中:
go func() {
Walk(t1, c1)
close(c1)
}()
go func() {
Walk(t2, c2)
close(c2)
}()
两个通道c1和c2分别在不同的goroutine中生产和关闭。主goroutine中的for i := range c1循环会从c1接收值,同时从c2同步接收值(通过<-c2)。
当修改为单个goroutine时:
go func() {
Walk(t1, c1)
close(c1)
Walk(t2, c2)
close(c2)
}()
问题在于执行顺序:
- 先执行
Walk(t1, c1),向c1发送所有值 - 关闭
c1 - 执行
Walk(t2, c2),向c2发送所有值 - 关闭
c2
但主goroutine的代码:
for i := range c1 {
if i != <-c2 {
return false
}
}
这里range c1会在c1关闭后结束循环,但此时c2可能还没有开始发送数据(因为Walk(t2, c2)在关闭c1之后才执行)。当主goroutine尝试从c2接收数据时(<-c2),发送方(同一个goroutine)还在执行Walk(t1, c1),导致死锁。
更明显的死锁示例:
package main
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 1
close(ch1)
ch2 <- 2 // 这里会阻塞,因为没有人从ch2接收
close(ch2)
}()
// 主goroutine只从ch1接收
<-ch1
// 程序会在这里挂起,因为ch2的发送操作被阻塞
}
要避免这个问题,需要确保通道的生产和消费模式匹配。在你的练习中,保持两个独立的goroutine是正确的做法。

