Go Channel偶发无法接收最后一个值的问题排查
Go Channel 计数偶发不足且添加WaitGroup死锁问题分析
问题描述
我正在学习Go Channel,编写了测试代码:创建10个goroutine,每个向channel发送1000个数值1;另一个goroutine从channel接收值并累加到计数器中,用WaitGroup确保goroutine执行完毕后打印结果。但运行后计数器有时无法达到预期的10000,偶发为9999。尝试为接收goroutine添加另一个WaitGroup却引发死锁,请问代码存在什么问题?
测试代码
package main import ( "fmt" "runtime" "sync" "time" ) func main() { var counter int fmt.Println("\nWithout Channels -------") for i := 0; i < 10; i++ { go func() { for j := 0; j < 1000; j++ { // to simulate race condition time.Sleep(time.Duration(1)) counter++ } }() } runtime.Gosched() fmt.Println("Expected counter: 10000, Actual counter:", counter) fmt.Println("\nWith Channels -------") for i := 0; i < 100; i++ { WithChannels() } } func WithChannels() { var counter int ch := make(chan int) var wg sync.WaitGroup // var wg2 sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { defer wg.Done() for j := 0; j < 1000; j++ { time.Sleep(time.Duration(1)) ch <- 1 } }() } // wg2.Add(1) go func() { for { increment, ok := <-ch if !ok { // channel closed, break break } counter += increment } // wg2.Done() }() wg.Wait() // wg2.Wait() // time.Sleep(time.Duration(1) * time.Second) fmt.Println("Expected counter: 10000, Actual counter:", counter) close(ch) }
运行输出
Without Channels ------- Expected counter: 10000, Actual counter: 139 With Channels ------- Expected counter: 10000, Actual counter: 9999 Expected counter: 10000, Actual counter: 10000 Expected counter: 10000, Actual counter: 9999 Expected counter: 10000, Actual counter: 10000 Expected counter: 10000, Actual counter: 10000 Expected counter: 10000, Actual counter: 9999 ...
问题根源分析
1. 计数偶发9999的原因
代码在wg.Wait()等待所有发送goroutine完成后,直接打印计数器,再关闭channel。这里存在时序漏洞:当最后一个发送goroutine把值写入channel后,wg.Done()触发wg.Wait()返回,但接收goroutine可能还没来得及读取这个值,此时打印的计数器就会少算未处理的数值,导致偶发9999。
2. 添加wg2引发死锁的原因
如果启用注释中的wg2相关代码,流程会变成:
wg.Wait()等待发送goroutine完成- 调用
wg2.Wait()等待接收goroutine完成 - 但此时channel未关闭,接收goroutine会一直阻塞在
<-ch上,永远无法执行wg2.Done(),主goroutine会无限等待,最终引发死锁。
正确解决方法
调整时序逻辑:等所有发送goroutine完成后,先关闭channel,再等待接收goroutine处理完所有剩余值,最后打印结果。
修改后的WithChannels()函数:
func WithChannels() { var counter int ch := make(chan int) var wg sync.WaitGroup var wg2 sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { defer wg.Done() for j := 0; j < 1000; j++ { time.Sleep(time.Duration(1)) ch <- 1 } }() } wg2.Add(1) go func() { defer wg2.Done() // 用range遍历channel,channel关闭后会自动退出循环 for increment := range ch { counter += increment } }() wg.Wait() // 等待所有发送goroutine完成 close(ch) // 关闭channel,通知接收goroutine无新值 wg2.Wait() // 等待接收goroutine处理完所有值 fmt.Println("Expected counter: 10000, Actual counter:", counter) }
关键修改点
- 发送完成后先关闭channel:关闭后接收端的
range遍历会处理完所有剩余值后自动退出 - 等待接收goroutine完成后再打印计数器,确保所有值都被累加
- 用
for range替代手动判断channel状态的无限循环,代码更简洁可靠
另外补充:原代码中Without Channels部分的问题是竞态条件,多个goroutine无同步地修改counter,需要用sync.Mutex或原子操作保护变量修改。
内容的提问来源于stack exchange,提问作者razzz
相关产品推荐
相关产品推荐

