Golang 调度器四个重要结构 :M P G Sched
GMP的结构源码在文件中\src\runtime\runtime2.go
简介
- G:goroutine,go程序建立的用户线程。主要保存 goroutine 的运行时栈信息(stack结构体)以及 CPU 的一些寄存器的值(gobuf结构体),还有关联的M,全局队列中下个G等信息。
-
M:machine 一个
M
直接关联一个os内核线程,用于执行G。M
会优先从关联的P
的本地队列中直接获取待执行的G
,它保存了 M 自身使用的栈信息、当 前正在 M 上执行的 G 信息、与之绑定的 P 信息。 -
P:processor 代表了
M
所需的上下文环境,也是处理用户级代码逻辑的处理器,可以看作一个局部调度器使go代码在一个线程上跑。 -
P列表:在创建程序的时候创建一个
P
列表, 最多有$GOMAXPROCS个,这环境变量可以通过操作系统中的环境变量设置,也可以通过Go程序中的runtime.GOMAXPROCS()函数设置,默认为处理器的核心数,它代表了真正的并发度。 -
M列表:当前操作系统分配到当前go程序的内核线程数,可以通过go语言中runtime/debug包中的SetMaxThreads函数设置。当有一个
M
阻塞,会有一个新的M被创建;当有一个M
空闲,会被回收或睡眠。 -
P的本地队列:P维护一个runq_用来存放等待执行的goroutine,新创建的
G
会优先放在P
的本地队列,当本地队列满(256G)时,会放入G
的全局队列。 -
全局队列:如果
P
的本地队列已满,待执行的G
就会放在全局队列中,M
会先从关联的P
的本地队列中获取待执行的G
,没有的话,再到全局队列中获取;如果这里也没有了,就去其他P
的本地队列中获取一些任务。
GMP调度
上图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文(context)P,一个上下文连接一个或者多个Goroutine。图中P正在执行的Goroutine
为蓝色的;处于待执行状态的Goroutine
为灰色的,灰色的Goroutine
形成了一个队列runqueues
。P(Processor)的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数runtime.GOMAXPROCS()
进行设置。Processor数量固定意味着任意时刻只有固定数量的线程在运行go代码。Goroutine中就是我们要执行并发的代码。
为何要维护多个上下文P?因为当一个OS线程被阻塞时,P可以转而投奔另一个OS线程!
下图中看到,当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。调度器保证有足够的线程来运行所以的context P。一个很简单的例子就是系统调用syscall
,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine
被调度执行。
如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能本身就存在,没创建),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行Goroutine
队列中的其他Goroutine
。
当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,然后自己放到线程池或者转入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了一个上下文P闲着没事儿干而系统却任然忙碌。但是如果global runqueue没有任务G了,那么P就不得不从其他的上下文P那里拿一些G来执行。一般来说,如果上下文P从其他的上下文P那里要偷一个任务的话,一般就‘偷’run queue的一半,这就确保了每个OS线程都能充分的使用。
有疑问加站长微信联系(非本文作者)