复用WaitGroup引发死锁:是否需创建新的WaitGroup?
嘿,我来帮你拆解下这个死锁问题的根源,以及对应的解决办法~
首先得明确一个关键点:sync.WaitGroup是设计用来一次性等待一组goroutine完成的,绝对不能在调用过Wait()之后复用它。
回到你的代码里,第一个阶段你用wg来等待所有func1的goroutine完成,并且启动了一个goroutine执行wg.Wait(),当所有func1都调用wg.Done()后,wg的计数归0,这个Wait()就返回了,同时关闭了ch。
但接下来你又把同一个wg拿来给func2的goroutine用:遍历ch的时候每次调用wg.Add(1),然后启动func2,再启动另一个goroutine执行wg.Wait()来等待func2完成。
这里的问题在于,WaitGroup内部有一个状态标记,当Wait()返回后,它的内部状态就进入了“等待完成”的终态,后续再调用Add()和Done()都不会正确更新计数,也不会唤醒后续的Wait()调用。这就导致第二个wg.Wait()会永远阻塞,而负责关闭ch2的goroutine卡在这里,主goroutine又在等着遍历ch2,最终整个程序所有goroutine都进入休眠状态,触发死锁错误。
而你用新的wg2就能正常运行,是因为wg2是全新的,没有被调用过Wait(),它的计数从0开始,Add()、Done()和Wait()的流程都是完全符合预期的,不会有状态混乱的问题。
最直接也最安全的解决方式就是给不同阶段的并发任务使用独立的WaitGroup,就像你注释掉的wg2那样。这里给你整理好可以直接运行的修正代码:
package main import ( "fmt" "sync" "time" ) func main() { wg1 := sync.WaitGroup{} ch := make(chan int) for a := 0; a < 3; a++ { wg1.Add(1) go func1(int(3-a), ch, &wg1) } go func() { wg1.Wait() close(ch) }() wg2 := sync.WaitGroup{} ch2 := make(chan string) for val := range ch { fmt.Println(val) wg2.Add(1) go func2(val, ch2, &wg2) } go func() { wg2.Wait() close(ch2) }() for val := range ch2 { fmt.Println(val) } } func func1(seconds int, ch chan<- int, wg *sync.WaitGroup) { defer wg.Done() time.Sleep(time.Duration(seconds) * time.Second) ch <- seconds } func func2(seconds int, ch chan<- string, wg *sync.WaitGroup) { defer wg.Done() ch <- "hello" }
另外再补充一个小知识点:WaitGroup的官方文档明确标注了WaitGroup must not be copied after first use,而且复用调用过Wait()的WaitGroup属于未定义行为,所以在实际开发中,一定要让每个WaitGroup只负责一组相关的goroutine等待任务,用完就丢弃,不要复用。
内容的提问来源于stack exchange,提问作者temporarya

