Go的调度器内部有四个重要的结构:M,P,S,Sched
(定义在源码的src/runtime/runtime.h文件中)
一、G代表一个Goroutine对象,每次go调用的时候,都会创建一个G对象,它有自己的栈,Instruction Pointer和其他信息(正在等待的channel等等),用于调度。
二、M代表内核级线程,一个M就是一个线程,Goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的Goroutine、随机数发生器等等非常多的信息。
三、P代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在一个CPU核上执行一样,它的主要用途就是用来执行Goroutine的,所以它也维护了一个Goroutine队列,里面存储了所有需要它来执行的Goroutine。
四、Sched代表调度器,它维护存储M和G的队列以及调度器的一些状态信息等。
调度实现注意要点
-
P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个Goroutine可以同时运行(最大256)。
-
每一个P保存着本地G任务队列(RunQueue),同时会有一个全局G任务队列(Global RunQueue)。
每次go调用的时候
-
创建一个G对象,加入到本地队列或者全局队列
-
如果还有空闲的P,则创建一个M
-
M会启动一个底层线程,循环执行能找到的G任务
-
G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找,之后再去其它P中找(一次性转移一半)
-
以上的G任务执行是按照队列顺序(也就是go调用的顺序)执行的。
当线程阻塞时
-
当一个OS线程M0陷入阻塞时,P转而去运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。
-
当MO返回时,它必须尝试取得一个P来运行Goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来。
-
如果没有拿到的话,它就把Goroutine放在Global RunQueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查Global RunQueue并运行其中的Goroutine,否则Global RunQueue上的Goroutine永远无法执行。
Goruntinue如何调度
系统在启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:
-
记录所有P的G任务计数schedtick,schedtick会在每执行一个G任务后递增
-
如果检查到 schedtick一直没有递增,说明这个P一直在执行同一个G任务,如果超过一定的时间(10ms),就在这个G任务的栈信息里面加一个标记
-
当G任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个G
-
如果没有遇到非内联函数(有时候正常的小函数会被优化成内联函数)调用的话,那就惨了,会一直执行这个G任务,直到它自己结束;如果是个死循环,并且GOMAXPROCS=1的话,恭喜你,夯住了
-
所以Goroutine是按照抢占式调度的,一个Goroutine最多执行10ms就会换作下一个
G任务中断后的恢复
-
中断的时候将寄存器里的栈信息,保存到自己的G对象里面
-
当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了