yifhao 在微信朋友圈晒出这么一道题:
同事定位一个协程池的问题,发现简单的一行不同,却相差这么大。(Go版本1.12、1.13结果类似,Go1.14正常)
```go
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(1000000) // 1)使用这行大约 TotalAlloc = 511 MiB 花费3s
for i := 0; i < 1000000; i++ {
//wg.Add(1) // 2)使用这行大约 TotalAlloc = 74 MiB 花费0.5s
go func() {
time.Sleep(time.Duration(1) * time.Millisecond)
wg.Done()
}()
}
wg.Wait()
mem := runtime.MemStats{}
runtime.ReadMemStats(&mem)
curMem := mem.TotalAlloc / 1024 / 1024
fmt.Printf("\tTotalAlloc = %v MiB \n", curMem)
}
```
解释:
很多时候查问题就像是做一道智力题,表象是这样,然而实际原因和表象却不相干。
表象是waitgroup问题,然而从waitgroup原理和并发冲突去理解的都是歧途,一开始我也是这么想的.
其实是go1.14之前的tight loop不能被gc中断导致的问题.
大概生成上千次个协程时, 这时候内存到了gc阈值, sleep执行时会分配timer, 这时知道要gc了,告诉所有协程和P, 现在要gc了, 你们要停止.加了wg.Add(1)的那个, 正好Add方法有点复杂, 调用前有抢占点检查, 所以主协程可以被gc中断,gc得以执行完,而sleep完后的协程可以被复用.
而先Add(1000000)的那个, 主协程的for循环中没有检查自身被中断的地方. 虽然主协程也能在分配g时得知要gc了, 但是创建一个协程是由g0和systemstack执行, 这里面不能发起gc, 就返回继续执行. 所以主流程协程一直在运行.
调度timer的线程需要P才能恢复sleep的协程, 所有的P都处于gc暂停态, 使得sleep的协程也不能恢复并运行完,也就不能被复用. 主流程就一直分配新的协程, 之前那些sleep的协程不能被复用, 内存和CPU都增加很多.
更多评论