固定数量worker协程模式下WaitGroup调用竞态条件问题咨询
你的判断完全正确,该竞态确实是**子goroutine执行wg.Add的同时主goroutine调用wg.Wait**导致的。sync.WaitGroup的官方设计要求明确规定:所有wg.Add调用必须在对应的wg.Wait调用触发前完成,两者并发执行会直接触发内部计数器的读写竞态,符合你拿到的竞态检测报告特征。
你当前代码的执行时序漏洞非常典型:主goroutine往channel发送完9个任务后立刻调用wg.Wait,此时部分任务可能还没有被worker goroutine消费到,对应的wg.Add还没执行,WaitGroup内部计数器可能还是0,wg.Wait会直接返回,甚至出现任务未执行完就关闭channel的问题。
wg.Add并非必须全部在主goroutine中调用,核心约束只有一个:对应同一个Wait阶段的wg.Add操作,必须全部在wg.Wait触发前完成。
针对任务数量不固定、后续持续流入的场景,最常用的改造方案是把wg.Add调用移动到任务发送侧,每次发送任务前先执行wg.Add(1),不需要提前预知任务总数,发多少就计数多少,只要你确认所有任务都已发送完成后再调用wg.Wait即可。
修改后可运行的无竞态代码示例
func Test(t *testing.T) { t.Run("", func(t *testing.T) { var wg sync.WaitGroup queuedTaskC := make(chan func()) // 启动固定数量worker for i := 0; i < 5; i++ { wID := i + 1 go func(workerID int) { // 消费到channel关闭自动退出 for task := range queuedTaskC { task() } }(wID) } taskFn := func() { fmt.Println("executing task...") wg.Done() } // 每次发送任务前先Add,保证Add操作全部在Wait前完成 for i := 0; i < 9; i++ { wg.Add(1) queuedTaskC <- taskFn } // 所有任务发送完成后再等待执行完毕 wg.Wait() close(queuedTaskC) fmt.Println(len(queuedTaskC)) }) }
扩展场景适配
如果你的任务是由其他goroutine异步发送的,只需要保证所有发送任务的goroutine都遵守「发任务前先wg.Add(1)」的规则,等你收到所有任务都已发送完成的信号后,再调用wg.Wait即可正常运行。
如果需要同时等待worker goroutine全部退出,可以再新增一个独立的WaitGroup专门统计worker的生命周期,关闭任务channel后等待所有worker退出即可。
内容的提问来源于stack exchange,提问作者theITguy

