Go并发FanIn模式同时收发引发死锁的原因探究
Go FanIn模式死锁问题解析
问题场景
你实现的FanIn模式代码运行后输出部分值就触发死锁,修改接收逻辑后程序恢复正常,下面是具体分析:
错误代码
package main import ( "fmt" ) func main() { even := make(chan int) odd := make(chan int) quit := make(chan int) fanin := make(chan int) go send(even, odd, quit) go receive(even, odd, quit, fanin) for v := range fanin { fmt.Println(v) } fmt.Println("about to exit") } // send channel func send(even, odd, quit chan<- int) { for i := 0; i < 100; i++ { if i%2 == 0 { even <- i } else { odd <- i } } close(quit) } // receive channel func receive(even, odd, quit <-chan int, fanin chan<- int) { thisLoop: for { select { case fanin <- <-even: // 问题出在这里 case fanin <- <-odd: case <-quit: break thisLoop } } close(fanin) }
死锁报错
... 95 96 99 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /home/floosde/go/src/floosde/main.go:17 +0x194 goroutine 19 [chan receive]: main.receive(...) /home/floosde/go/src/floosde/main.go:41 created by main.main in goroutine 1 /home/floosde/go/src/floosde/main.go:15 +0x137 exit status 2
修复后的代码
// receive channel func receive(even, odd, quit <-chan int, fanin chan<- int) { thisLoop: for { select { case v := <-even: // 修复后的写法 fanin <- v case v := <-odd: // 修复后的写法 fanin <- v case <-quit: break thisLoop } } close(fanin) }
核心原因解析
问题的关键在于Go语言select语句的执行逻辑,以及两种写法的表达式求值顺序差异:
错误写法的致命问题
对于case fanin <- <-even:,Go会先执行<-even(从even通道接收值),完成这个求值后,才会判断fanin <- 接收的值这个发送操作是否可执行。- 当send函数发送完所有100个值并关闭quit后,even/odd通道可能已无剩余值,此时quit通道处于关闭状态(
<-quit会立即返回)。 - 如果select循环选中了
fanin <- <-even:这个case,而even通道已经没有值,那么<-even会直接阻塞,程序无法再处理<-quit的case——因为求值<-even的过程已经卡住,根本没到select选择分支的阶段。 - 最终导致:receive goroutine阻塞在
<-even的求值上,main goroutine阻塞在range fanin(因为fanin未被关闭),所有goroutine进入休眠,触发死锁。
- 当send函数发送完所有100个值并关闭quit后,even/odd通道可能已无剩余值,此时quit通道处于关闭状态(
正确写法的逻辑合理性
修复后的case v := <-even:是标准的接收分支,select会将其作为独立的可执行分支评估:- 每次进入select,Go会同时检查所有分支的可执行性:如果even有值,就接收值并发送到fanin;如果even没值,就跳过这个分支,去检查其他分支(比如已关闭的quit通道)。
- 当quit通道关闭后,
<-quit是立即可执行的,select会选中这个分支,触发循环退出,随后关闭fanin,main goroutine的range循环正常结束,程序无死锁。
简单来说,错误写法把“从even取值”和“向fanin发值”绑成了不可拆分的操作,导致取值阻塞时无法响应quit信号;而正确写法将两个操作拆分,让select能灵活处理所有分支,包括quit信号。
内容的提问来源于stack exchange,提问作者Блабла Блаблов
相关产品推荐
相关产品推荐

