为何wg.Done()放置位置不同会触发sync.WaitGroup重用报错
问题原因解析
首先明确两个核心规则:
sync.WaitGroup官方使用规范明确要求:当Wait()方法已经被调用后,在该Wait()返回前,不允许再调用Add()方法将计数器从0改为正数值,否则就会抛出WaitGroup is reused before previous Wait has returned错误。- Go语言
defer语句执行规则:只有defer语句本身被执行到时,对应的延迟函数才会被注册到函数返回栈;如果代码在走到defer语句前就返回,该延迟函数不会执行。
第一种写法(defer wg.Done()放在worker开头)报错原因
你的代码中wg是全局变量,同时有1000个callWorker goroutine并发操作同一个WaitGroup,每个callWorker的执行逻辑为:打印日志→调用wg.Add(1)→启动worker goroutine→调用wg.Wait()。
当defer wg.Done()放在worker开头时,worker函数只要被调用,第一行就会把wg.Done()注册为延迟函数,不管后续走哪个分支返回,wg.Done()一定会执行。
此时极易出现如下触发错误的时序:
- goroutine A执行
wg.Add(1),wg计数器变为1 - goroutine A启动worker goroutine,worker很快执行完逻辑,调用
wg.Done(),wg计数器回到0 - 此时goroutine B刚好执行到
wg.Add(1),把wg计数器从0改为1 - 但goroutine A此时还处于
wg.Wait()调用过程中,还未返回,遇到计数器从0被改回正数值的情况,直接触发预设报错。
第二种写法未报错的本质
你的worker函数中存在恒成立的if true { return nil }逻辑,当你把defer wg.Done()放在该if分支之后,代码走到if就直接返回,永远不会执行到defer wg.Done()这一行,因此wg.Done()从来不会被调用,wg的计数器只会持续增加,永远不会降到0。
所有callWorker中的wg.Wait()都会永久阻塞,不会出现Wait()调用过程中计数器从0被改回正数的场景,自然不会抛出对应错误。
注意:第二种写法本身是错误用法,并非“正常运行”,只是没有触发你看到的报错而已,实际上所有worker的Done通知都没有发出,Wait永远不会返回,会造成goroutine泄露。
内容的提问来源于stack exchange,提问作者Yongqi Z
相关产品推荐
相关产品推荐

