聊聊Go GMP调度模型

作者:杨润炜
日期:2022/3/13 22:38

Go语言最广为人知的特性,应该就是goroutine了吧。我们都知道它能够并发地执行给定的操作,且能够稳定运行大量的goroutine,比线程进程性能强太多了,管理也很方便。挺好奇Go是怎样做到的,接下来就来瞧瞧。

不成熟的版本 — GM

go-gm-3

在GMP之前,Go的调度模型是GM,它们分别是:

  • G: goroutine
  • M: os thread(内核级线程)

存在的问题:

  • 全局队列加锁导致竞争,成为性能瓶颈;
  • G在多个M间切换,导致内存缓存的局部性/亲缘性差;
  • 新的G由某个M创建后,放入全局队列,然后再被调度,增加了不必要的开销;
    改进方案当然就是即将上场的GMP模型了。

GMP调度模型

先来个图总览一下:
go-gmp

从Go1.2开始,调度模型引入了P。它的处理器(Process)的抽象,但其实不是真正的CPU,能够分配到CPU资源的还是M。
P是怎样处理上面的问题的呢?

  • P与M结合构成一个并行处理的单元,它限制了并行的任务数量,runtime.GOMAXPROCS 能够配置它的数量;
  • G需要挂到PM上面才能执行;
  • 每个P都有一个队列(lock free),存储当前绑定的G,M会优先从绑定的P的队列中获取G,避免加锁到全局队列中竞争获取,新创建的G也是直接放入这个本地队列,减少放全局队列的各种开销;
  • 由P管理着内存缓存,M与G会尽量保持与同个P配对,保证了P-M-G的内存局部性/亲缘性;

引入更多的管理策略,优化调度器性能

任务盗取(work-stealing)会自旋检查

P如果运行完队列中的G,则能够盗取其它P的G或从全局队列中获取G

线程自旋(spinning thread)

调度器会保证至少有一个M在自旋检查P和G有没有可绑定的,避免任务等待,但自旋也会浪费一些CPU算力。

抢占式调度

sysmon会检查长时间运行的G,将其中断并重新放入调度。中断的原理是sysmon通过信号量通知G的M,往G的PC中插入特定指令,G执行该指令后将自己推入全局队列重新调度。

系统调用阻塞应对策略(syscall)

当G在执行阻塞调用时,M也会跟着阻塞并解绑P,但此时的P处于(syscall)不能调度给别的M。

  • 如果M很快从阻塞中唤醒,则重新绑定到原先的P,保证数据亲和性;
  • 如果超出P的等待阈值,则由sysmon负责将该P设置为空闲,重新投入到调度中;

需要注意的

小心线程溢出

因为runtime.GOMAXPROCS只是限制了P的数量(M的数量上限比较大),如果存在大量耗时长的系统调用,会导致创建大量的M并阻塞着,可能会导致系统资源耗尽。

参考

Scalable Go Scheduler Design Doc
Linux下调用pthread库创建的线程是属于用户级线程还是内核级线程?

感谢您的阅读!
如果看完后有任何疑问,欢迎拍砖。
欢迎转载,转载请注明出处:http://www.yangrunwei.com/a/126.html
邮箱:glowrypauky@gmail.com
QQ: 892413924