您好,欢迎访问一九零五行业门户网

再见 Go 面试官:GMP 模型,为什么要有 P?

今天的主角,是 go 面试的万能题 gmp 模型的延伸题(疑问),那就是 ”gmp 模型,为什么要有 p?“
进一步推敲问题的背后,其实这个面试题本质是想问:”gmp 模型,为什么不是 g 和 m 直接绑定就完了,还要搞多一个 p 出来,那么麻烦,为的是什么,是要解决什么问题吗?“
这篇文章煎鱼就带你一同探索,gm、gmp 模型的变迁是因为什么原因。
gm 模型在 go1.1 之前 go 的调度模型其实就是 gm 模型,也就是没有 p。
今天带大家一起回顾过去的设计。
解密 go1.0 源码我们了解一个东西的办法之一就是看源码,和煎鱼一起看看 go1.0.1 的调度器源码的核心关键步骤:
static voidschedule(g *gp){ ... schedlock(); if(gp != nil) { ... switch(gp->status){ case grunnable: case gdead: // shouldn't have been running! runtime·throw("bad gp->status in sched"); case grunning: gp->status = grunnable; gput(gp); break; } gp = nextgandunlock(); gp->readyonstop = 0; gp->status = grunning; m->curg = gp; gp->m = m; ... runtime·gogo(&gp->sched, 0);}
调用 schedlock 方法来获取全局锁。获取全局锁成功后,将当前 goroutine 状态从 running(正在被调度) 状态修改为 runnable(可以被调度)状态。调用 gput 方法来保存当前 goroutine 的运行状态等信息,以便于后续的使用。调用 nextgandunlock 方法来寻找下一个可运行 goroutine,并且释放全局锁给其他调度使用。获取到下一个待运行的 goroutine 后,将其运行状态修改为 running。调用 runtime·gogo 方法,将刚刚所获取到的下一个待执行的 goroutine 运行起来,进入下一轮调度。思考 gm 模型通过对 go1.0.1 的调度器源码剖析,我们可以发现一个比较有趣的点。那就是调度器本身(schedule 方法),在正常流程下,是不会返回的,也就是不会结束主流程。
g-m模型简图他会不断地运行调度流程,goroutinea 完成了,就开始寻找 goroutineb,寻找到 b 了,就把已经完成的 a 的调度权交给 b,让 goroutineb 开始被调度,也就是运行。
当然了,也有被正在阻塞(blocked)的 g。假设 g 正在做一些系统、网络调用,那么就会导致 g 停滞。这时候 m(系统线程)就会被会重新放内核队列中,等待新的一轮唤醒。
gm 模型的缺点这么表面的看起来,gm 模型似乎牢不可破,毫无缺陷。但为什么要改呢?
在 2012 年时 dmitry vyukov 发表了文章《scalable go scheduler design doc》,目前也依然是各大研究 go 调度器文章的主要对象,其在文章内讲述了整体的原因和考虑,下述内容将引用该文章。
当前(代指 go1.0 的 gm 模型)的 goroutine 调度器限制了用 go 编写的并发程序的可扩展性,尤其是高吞吐量服务器和并行计算程序。
实现有如下的问题:
存在单一的全局 mutex(sched.lock)和集中状态管理:mutex 需要保护所有与 goroutine 相关的操作(创建、完成、重排等),导致锁竞争严重。goroutine 传递的问题:goroutine(g)交接(g.nextg):工作者线程(m's)之间会经常交接可运行的 goroutine。上述可能会导致延迟增加和额外的开销。每个 m 必须能够执行任何可运行的 g,特别是刚刚创建 g 的 m。每个 m 都需要做内存缓存(m.mcache):会导致资源消耗过大(每个 mcache 可以吸纳到 2m 的内存缓存和其他缓存),数据局部性差。频繁的线程阻塞/解阻塞:在存在 syscalls 的情况下,线程经常被阻塞和解阻塞。这增加了很多额外的性能开销。gmp 模型为了解决 gm 模型的以上诸多问题,在 go1.1 时,dmitry vyukov 在 gm 模型的基础上,新增了一个 p(processor)组件。并且实现了 work stealing 算法来解决一些新产生的问题。
gmp 模型,在上一篇文章《go 群友提问:goroutine 数量控制在多少合适,会影响 gc 和调度?》中已经讲解过了。
觉得不错的小伙伴可以关注一下,这里就不再复述了。
带来什么改变加了 p 之后会带来什么改变呢?我们再更显式的讲一下。
每个 p 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 gm 模型的性能开销大头就是锁竞争。
每个 p 相对的平衡上,在 gmp 模型中也实现了 work stealing 算法,如果 p 的本地队列为空,则会从全局队列或其他 p 的本地队列中窃取可运行的 g 来运行,减少空转,提高了资源利用率。
为什么要有 p这时候就有小伙伴会疑惑了,如果是想实现本地队列、work stealing 算法,那为什么不直接在 m 上加呢,m 也照样可以实现类似的功能。
为什么又再加多一个 p 组件?
结合 m(系统线程) 的定位来看,若这么做,有以下问题。
一般来讲,m 的数量都会多于 p。像在 go 中,m 的数量最大限制是 10000,p 的默认数量的 cpu 核数。另外由于 m 的属性,也就是如果存在系统阻塞调用,阻塞了 m,又不够用的情况下,m 会不断增加。
m 不断增加的话,如果本地队列挂载在 m 上,那就意味着本地队列也会随之增加。这显然是不合理的,因为本地队列的管理会变得复杂,且 work stealing 性能会大幅度下降。
m 被系统调用阻塞后,我们是期望把他既有未执行的任务分配给其他继续运行的,而不是一阻塞就导致全部停止。
因此使用 m 是不合理的,那么引入新的组件 p,把本地队列关联到 p 上,就能很好的解决这个问题。
总结今天这篇文章结合了整个 go 语言调度器的一些历史情况、原因分析以及解决方案说明。
”gmp 模型,为什么要有 p“ 这个问题就像是一道系统设计了解,因为现在很多人为了应对面试,会硬背 gmp 模型,或者是泡面式过了一遍。而理解其中真正背后的原因,才是我们要去学的要去理解。
知其然知其所以然,才可破局。
以上就是再见 go 面试官:gmp 模型,为什么要有 p?的详细内容。
其它类似信息

推荐信息