为何偶发sync: negative WaitGroup counter panic?切片并发写入验证异常
切片并发写入导致WaitGroup负计数panic的原因
问题场景
编写了一段验证切片并发写入行为的代码,运行时偶尔触发sync: negative WaitGroup counter的panic(非必现);但将切片容量设置为大于2时,panic现象消失。
相关代码
func main() { for i := 0; i < 10000; i++ { // 当容量>2时,panic消失 b := make([]int, 2, 2) b[0], b[1] = 0, 1 wg := sync.WaitGroup{} wg.Add(1) go func() { b = append(b, 3) wg.Done() }() wg.Add(1) go func() { b = append(b, 4) wg.Done() }() wg.Wait() } }
错误信息
panic: sync: negative WaitGroup counter goroutine 12821 [running]: sync.(*WaitGroup).Add(0x0?, 0xc0000f4ba0?) /usr/local/go/src/sync/waitgroup.go:83 +0xda sync.(*WaitGroup).Done(...) /usr/local/go/src/sync/waitgroup.go:108 main.main.func1() /Users/liushi/Projects/go/golang-demo/main.go:54 +0x97 created by main.main /Users/liushi/Projects/go/golang-demo/main.go:48 +0x11f Process finished with the exit code 2
原因分析
这个panic的核心原因是栈上的切片扩容操作破坏了相邻WaitGroup的内存结构,具体过程如下:
- 当使用
make([]int,2,2)创建切片时,由于容量极小,Go编译器会将切片的底层数组分配在main函数的栈上,且与同栈帧的wg变量内存位置相邻。 - goroutine执行
append(b,3)时,因为切片容量不足触发扩容,需要将栈上的底层数组拷贝到堆上的新数组中。 - 极端情况下,这个拷贝操作可能出现内存越界,直接破坏了相邻
wg变量的内部结构(比如篡改了WaitGroup的计数器字段)。 - 当被破坏的
wg执行Done()时,内部计数器会被错误地修改为负数,触发sync: negative WaitGroup counter的panic。 - 当切片容量设置为大于2时,Go会将切片底层数组直接分配在堆上,扩容时的拷贝操作不会影响栈上的
wg变量,因此不会出现内存破坏的问题,panic也就消失了。
另外补充一点:WaitGroup是值类型,且其方法依赖内部的原子计数器,任何对其内存结构的非法修改都会导致不可预期的错误,这也是这类panic非必现的原因——只有当内存越界恰好命中WaitGroup的关键字段时才会触发。
内容的提问来源于stack exchange,提问作者Derrick
相关产品推荐
相关产品推荐

