为何使用-race标志运行Go原子计数器代码时结果不符预期?
搞定Go原子计数器在-race模式下的异常问题
嘿,我刚看完你的问题,这情况其实是典型的WaitGroup使用不当加上竞态检测放大了问题导致的,咱们一步步拆解:
问题到底出在哪?
你说不用-race跑结果是对的,一加竞态检测就少了,核心原因不是原子操作本身(atomic.AddInt64绝对是线程安全的),而是你代码里WaitGroup的调用时机错了,再加上runtime.Gosched()推波助澜。
大概率你的代码是这么写的(从结果反推):
var wg sync.WaitGroup counter := atomicCounter{} for i := 0; i < 1000000; i++ { go func() { wg.Add(1) // 错误:在goroutine内部调用wg.Add counter.Add(1) defer wg.Done() }() } wg.Wait() fmt.Println(counter.Value())
- 不开启
-race时,goroutine调度没那么频繁,主goroutine循环启动1000000个goroutine的速度,赶不上大部分子goroutine执行wg.Add(1)的速度,所以wg.Wait()能等到所有子goroutine完成,结果正确。 - 开启
-race后,Go运行时会主动增加调度频率,主goroutine可能在很多子goroutine还没来得及执行wg.Add(1)的时候,就已经跑到wg.Wait()了。这时候WaitGroup的计数还是0,直接返回,主goroutine提前打印结果,而后面的子goroutine再执行Add也没用了。
另外你在Add方法里加的runtime.Gosched()会让子goroutine主动让出CPU,主goroutine更快到达wg.Wait(),直接加剧了这个问题。
怎么修复?
两步就能搞定:
把
wg.Add移到goroutine外面:
必须在启动goroutine之前就把WaitGroup的计数加上,保证主goroutine调用wg.Wait()时,计数已经是正确的1000000。比如:var wg sync.WaitGroup counter := atomicCounter{} total := 1000000 wg.Add(total) // 一次性加总计数,或者循环里每次加1都可以 for i := 0; i < total; i++ { go func() { defer wg.Done() counter.Add(1) }() } wg.Wait() fmt.Println(counter.Value())删掉没必要的
runtime.Gosched():
原子操作本身已经保证了线程安全,这里主动让出CPU完全没有意义,还会影响性能,直接删掉就行。
验证一下
修复后不管你加不加-race标志,运行结果都会稳定在1000000。其实竞态检测帮了你个大忙——它把你代码里隐藏的WaitGroup使用错误给揪出来了,不然这个bug可能在生产环境里偶尔出现,更难排查。
内容的提问来源于stack exchange,提问作者Roman
相关产品推荐
相关产品推荐

