具体详细的内容可以看 小徐先生的编程世界 ,讲的非常好,也非常详细。
记录一点个人心得:
-
m本质上就是操作系统内核级的一个线程,协程的出现是为了减少线程频繁切换的开销,让多个协程并发的在一个线程中运行(因为用户态切换开销小)。但是协程如果出现阻塞,就会导致该线程直接阻塞,该线程内的其他协程无法运行。所以goroutine就加了一层p,成功将协程与线程进行了解藕,使得这种情况下,当一个协程阻塞的时候,可以把剩下的协程都切换到下一个线程中运行。
谁和谁解耦 无 P 有 P G 和 M 多个G 必须绑定在某个 M 上,M 死了 G 也得陪葬。 G 绑定在 P 上。只要 P 能动,G 就能动,不在乎下面垫着的 M 是谁。 M 和 资源 资源(队列/内存)绑在 M 身上。 资源绑在 P 身上。M 就像个“共享单车骑手”,随骑随走,车(P)坏了换一辆,人(M)累了换个人骑。 -
p是m运行g所需要的上下文。
-
每一个m都有一个g0作为调度者,去切换在在该m上的p中的协程。
-
被动唤醒和正常新建(为了性能)优先进本地队列插队,而主动让出和超时抢占(为了公平)则必须去全局队列排队。每61次的从全局队列队首拉取一个直接执行。
-
当一个g1在m1执行了很久的系统调用(比如读一个大文件),终于回来了。 但是此时,原来的 P1 已经被 sysmon 抢走给 M2 用了。此时如果有空闲的p,m1会拉一个p来执行g1,如果没有则会把g1放到全局队列中去。
-
P 还有一个隐形的作用是提升 CPU 缓存命中率。 Go 的调度策略会尽量让一个 G 创建的子 G,依然放入当前 P 的本地队列(甚至是
runnext插队位)。 这样能保证父子协程在同一个 CPU 核上运行,利用 L1/L2 热缓存,避免数据在不同核之间频繁迁移带来的开销。 -
P 的数量(
GOMAXPROCS)决定了并行的上限。 无论系统开了多少个线程(M),真正能同时执行用户代码的只有 P 的个数。P 是调度器对并发度进行人为控制的阀门。