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

6 回复

谢谢,我大概明白了。

更多关于Golang中在同一个Go协程关闭2个通道会导致死锁吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


它运行在不同的 goroutine 中,Chan 本质上是一个带锁的阻塞队列。 chan 接收一个值,如果没有数据就会被阻塞;发送一个值,如果 chan 已满也会被阻塞。

谢谢。我大概明白了。但我还有一个问题:为什么“发送20”必须等待range循环将其取出?我的意思是,在我看来,它是在不同的线程中,为什么我们不能独立地向通道发送数据呢?

感觉像是有一个互斥锁或类似的东西c1。

谢谢

  1. 你的“walker goroutine”启动
  2. 它进入 Walk(t1, c1)
  3. Walk(t1, c1)c1 发送第一个值
  4. 你的主 goroutine 在 for … range 循环中从 c1 接收到第一个值
  5. 你的主 goroutine 尝试从 c2 接收值,以便与从 c1 接收的值进行比较
  6. 你的“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)
}()

两个通道c1c2分别在不同的goroutine中生产和关闭。主goroutine中的for i := range c1循环会从c1接收值,同时从c2同步接收值(通过<-c2)。

当修改为单个goroutine时:

go func() {
    Walk(t1, c1)
    close(c1)
    Walk(t2, c2)
    close(c2)
}()

问题在于执行顺序:

  1. 先执行Walk(t1, c1),向c1发送所有值
  2. 关闭c1
  3. 执行Walk(t2, c2),向c2发送所有值
  4. 关闭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是正确的做法。

回到顶部