Goroutine的调度模型

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的队列以及调度器的一些状态信息等。

调度实现

注意要点

  1. P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个Goroutine可以同时运行(最大256)。

  2. 每一个P保存着本地G任务队列(RunQueue),同时会有一个全局G任务队列(Global RunQueue)。

每次go调用的时候

  1. 创建一个G对象,加入到本地队列或者全局队列

  2. 如果还有空闲的P,则创建一个M

  3. M会启动一个底层线程,循环执行能找到的G任务

  4. G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找,之后再去其它P中找(一次性转移一半)

  5. 以上的G任务执行是按照队列顺序(也就是go调用的顺序)执行的。
    在这里插入图片描述

当线程阻塞时

  1. 当一个OS线程M0陷入阻塞时,P转而去运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。

  2. 当MO返回时,它必须尝试取得一个P来运行Goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来。

  3. 如果没有拿到的话,它就把Goroutine放在Global RunQueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查Global RunQueue并运行其中的Goroutine,否则Global RunQueue上的Goroutine永远无法执行。
    在这里插入图片描述

Goruntinue如何调度

系统在启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:

  1. 记录所有P的G任务计数schedtick,schedtick会在每执行一个G任务后递增

  2. 如果检查到 schedtick一直没有递增,说明这个P一直在执行同一个G任务,如果超过一定的时间(10ms),就在这个G任务的栈信息里面加一个标记

  3. 当G任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个G

  4. 如果没有遇到非内联函数(有时候正常的小函数会被优化成内联函数)调用的话,那就惨了,会一直执行这个G任务,直到它自己结束;如果是个死循环,并且GOMAXPROCS=1的话,恭喜你,夯住了

  5. 所以Goroutine是按照抢占式调度的,一个Goroutine最多执行10ms就会换作下一个

G任务中断后的恢复

  1. 中断的时候将寄存器里的栈信息,保存到自己的G对象里面

  2. 当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了