Goroutine的调度

Posted SuPhoebe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Goroutine的调度相关的知识,希望对你有一定的参考价值。

本文整理自The Go scheduler

Goroutine的调度

Go语言之所以要自己实现一个调度器有以下两个原因:

  1. 协程调度。因为系统内核不能再决定协程的切换,那么协程的切换时间点则是由程序内部的调度器决定的。
  2. 垃圾回收。垃圾回收的必要条件是内存位于一致状态,这就需要暂停所有的线程,如果交给系统去做,那么会暂停所有的线程使其一致。程序自身的调度器知道什么时候内存位于一致状态,那么就没有必要暂停所有运行的协程。

调度模型简介

Go的调度器内部有三个重要的结构:M,P,S

M:Machine,代表内核级线程,内核线程是有操作系统进行管理的。M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。
P:Processor,上下文处理器,它的主要用途就是用来连接执行的goroutine和内核线程的,所以它也维护了一个未被执行的goroutine队列,里面存储了所有需要它来执行的goroutine。
G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。

调度器是sched,它没在上图中被标注出,它维护有存储M和G的队列以及调度器的一些状态信息等。

下面图我们看到他们之间的对应规则:一个M对应一个P,一个P下面挂多个G,但一个时候只有一个G在跑,其余都是放入等待队列,等待下一次切换时使用。

从上图中看,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。

P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度即有多少个goroutine可以同时运行。图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue)。

Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出一个goroutine执行。

那么假如一个运行的协程G调用syscall进入阻塞怎么办?如下图左边

G0进入阻塞,那么P会转移到另外一个内核线程M1(此时P与M还是1对1)。当syscall返回后,G0需要抢占一个P继续执行,如果抢占不到,G0挂入全局就绪队列runqueue,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查global runqueue并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行。

另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:

以上是关于Goroutine的调度的主要内容,如果未能解决你的问题,请参考以下文章

Go 协程(goroutine)调度原理

golang的goroutine调度机制

goroutine 调度算法

Go 语言调度: goroutine 调度器

goroutine 调度

goroutine 调度