Go1.14 以下版本 WaitGroup 一个奇怪的问题

polaris · 2020-03-30 23:22:50 · 2982 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2020-03-30 23:22:50 的主题,其中的信息可能已经有所发展或是发生改变。

yifhao 在微信朋友圈晒出这么一道题:

同事定位一个协程池的问题,发现简单的一行不同,却相差这么大。(Go版本1.12、1.13结果类似,Go1.14正常)

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都增加很多.


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2982 次点击  ∙  1 赞  
加入收藏 微博
4 回复  |  直到 2020-04-10 21:45:43
polaris
polaris · #1 · 5年之前

所以,其实这个问题跟 waitgroup 没直接关系

saberlong
saberlong · #2 · 5年之前

好例子,突破盲点。看到gc时,立马联系到1.14的更新内容。

aixinaxc
aixinaxc · #3 · 5年之前

那1.14是如何修复的呢

polaris
polaris · #4 · 5年之前

@aixinaxc 抢占调度

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传