Golang goroutine调度原理

goroutine简介

goroutinego 语言 中最为 NB 的设计,也是其魅力所在,goroutine 的本质是协程,是实现并行计算的核心。

goroutine 使用方式非常的简单,只需使用 go 关键字即可启动一个协程,并且它是处于异步方式运行,你不需要等它运行完成以后在执行以后的代码。

goroutine相关概念

并发

一个 cpu 上能同时执行多项任务,在很短时间内,cpu 来回切换任务执行(在某段很短时间内执行程序 a,然后又迅速得切换到程序 b 去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。

并行

当系统有多个 CPU 时,每个 CPU 同一时刻都运行任务,互不抢占自己所在的 CPU 资源,同时进行,称为并行。

进程

cpu 在切换程序的时候,如果不保存上一个程序的状态(也就是我们常说的 context–上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。

因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。

线程

cpu 切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需要内核态都需要读取用户态的数据,进程一旦多起来,cpu 调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。

协程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine 也是协程。

Golang goroutine调度模型

groutine 能拥有强大的并发实现是通过 GPM 调度模型实现,下面就来解释下 goroutine 的调度模型。

27_Go语言goroutine调度原理.png

Go 的调度器内部有四个重要的结构:M,P,S,Sched,如上图所示(Sched 未给出)。

M: ;M 是一个很大的结构,里面维护小对象内存 cache(mcache)、当前执行的 goroutine、随机数发生器等等非常多的信息。

G: 代表一个 goroutine,它有自己的栈,instruction pointer 和其他信息(正在等待的 channel 等等),用于调度。

P: P 全称是 Processor,处理器,它的主要用途就是用来执行 goroutine 的,所以它也维护了一个 goroutine 队列,里面存储了所有需要它来执行的 goroutine。

Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

Golang goroutine调度实现

28_Go语言goroutine调度原理.png

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

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

图中灰色的那些 goroutine 并没有运行,而是出于 ready 的就绪态,正在等待被调度。P 维护着这个队列(称之为 runqueue),Go 语言里,启动一个 goroutine 很容易:go function 就行,所以每有一个 go 语句被执行,runqueue 队列就在其末尾加入一个 goroutine,在下一个调度点,就从 runqueue 中取出(如何决定取哪个 goroutine?)一个 goroutine 执行。

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

29_Go语言goroutine调度原理.png

当 MO 返回时,它必须尝试取得一个P来运行 goroutine,一般情况下,它会从其他的 OS 线程那里拿一个 P 过来,
如果没有拿到的话,它就把 goroutine 放在一个 global runqueue 里,然后自己睡眠(放入线程缓存里)。所有的 P 也会周期性的检查 global runqueue 并运行其中的 goroutine,否则 global runqueue 上的 goroutine 永远无法执行。

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

30_Go语言goroutine调度原理.png

Golang goroutine调度原理总结

groutine 能拥有强大的并发实现是通过 GPM 调度模型实现,M 代表内核级线程,一个 M 就是一个线程,goroutine 就是跑在 M 之上的,G 代表一个 goroutine,它有自己的栈,P 全称是 Processor,处理器,它的主要用途就是用来执行 goroutine 的。