golang 协程调度原理
Go语言 最大的特点是提供简单易用的并发编程,这个并发的执行单元就是goroutine, 这个goroutine 是运行在用户态,并由GO自身来调度。调度器来决定谁来使用CPU资源,谁该让出CPU资源。 本文就来深入探讨一下Go的调度原理。
GMP调度模型
Go采用的是GMP调度模型。
核心概念
- G :即Goroutine ,使用关键字 go 即可创建一个协程来处理用户程序,如下所示:
 
1    go func() //创建协程来执行函数
- M :Machine 系统抽象的线程,代表真正的机器资源,目前最多10000,超过这个数量会panic.
 - P :Process,虚拟处理器,代表goroutine的上下文,用于关联G和M;P的数量可以通过GOMAXPROCS设置,默认为CPU核数;
 - 本地队列(local queue): 每个P关联有一个协程队列,该队列就是P的本地队列,新生成的协程放在该队列中,当该队列达到最大数量时,会将该队列的一般协程存入到全局队列中;
 - 全局队列(global queue): 当本地队列达到最大数量时,多余的协程就会存在全局队列中;
 
调度原理
 1     +-------------------- sysmon ---------------//------+ 
 2                             |                                                   |
 3                             |                                                   |
 4                +---+      +---+-------+                   +--------+          +---+---+
 5 go func() ---> | G | ---> | P | local | <=== balance ===> | global | <--//--- | P | M |
 6                +---+      +---+-------+                   +--------+          +---+---+
 7                             |                                 |                 | 
 8                             |      +---+                      |                 |
 9                             +----> | M | <--- findrunnable ---+--- steal <--//--+
10                                    +---+ 
11                                      |
12                                    mstart
13                                      |
14               +--- execute <----- schedule 
15               |                      |   
16               |                      |
17               +--> G.fn --> goexit --+ 
从上图(来自雨痕GO源码阅读)我们可以看到在新建G时
- 当使用go 关键字执行函数时,会创建(首先查看P的freelist是否可以复用G,如果不能则新建)一个G(goroutine);
 - 新创建的G,并不会添加到本地队列,而是添加到P关联的runnext中(runnext是一个指针变量,用来存放G的地址),runnext原来的G被放到本地队列中;
 
- 2.1 如果本地队列未满(最大256),则放置到队尾;
 - 2.2 如果本地队列已满,则将本地队列的一半数量的G和runnext中原来的G存放到全局队列中;
 
- 唤醒或新建M来执行任务。
 - 进入调度循环
 - 尽力获取可执行的G,并执行
 - 清理现场并且重新进入调度循环

 
运行时调度
- 为公平起见,有1/61的机会首先从全局队列获取到G,如果获取到则执行G;
 - 如果没有机会从全局队列获取或者没有获取到G,则从P关联的runnext或者本地队列获取:
2.1 如果P的runnext有G,则执行该G; 2.2 如果P的runnext没有G,则从本地队列中获取G; - 如果第二步没有获取到,则执行以下步骤获取:
3.1 从关联P中获取,步骤同2,若获取到返回;
3.2 从全局队列中获取,若获取到返回;
3.3 调用 netpoll()取异步调用结束的G,该调用为非阻塞调用,若获取到则返回一个G,剩余的G放入到全局队列中;
3.4 从其他P中steal一半的G到本地队列,若获取到则返回;
3.5 如果处于垃圾回收标记阶段,则执行垃圾回收操作;
3.6 再次从全局队列中获取,若获取到返回;
3.7 调用 netpoll()取异步调用结束的G,该调用为阻塞调用,若获取到则返回一个G,剩余的G放入到全局队列中; 
协程的状态
在go1.12.5/src/runtime/runtime2.go:15 定义有如下几个状态
_Gidle: 值(0) 刚刚被创建,还没有初始化;
_Grunnable: 值(1) 已经在运行队列中,只是此时没有执行用户代码,未分配栈;
_Grunning:值(2)在执行用户代码,已经不在运行队列中,分配了M和P;
_Gsyscall: 值(3)当前goroutine正在执行系统调用,已经不再运行队列中,分配了M;
_Gwaiting: 值(4) 在运行时被阻塞,并没有执行用户代码,此刻的goroutine会被记录到某处(例如channel等待队列)
_Gmoribund_unused: 值(5) 当前并未使用,但是已经在gdb中进行了硬编码;
_Gdead: 值(6) 当前goroutine没有被使用,可能刚刚退出或者刚刚被初始化,并没有执行用户代码;
_Genqueue_unused: 值(7) 当前并未使用;
_Gcopystack:值(8)正在复制堆栈,并未执行用户代码,也没有在运行队列中;
状态转换图(引自 goroutine调度)如下
 1                                                     +------------+
 2                                      ready           |            |
 3                                  +------------------ |  _Gwaiting |
 4                                  |                   |            |
 5                                  |                   +------------+
 6                                  |                         ^ park_m
 7                                  V                         | 
 8  +------------+            +------------+  execute   +------------+            +------------+    
 9  |            |  newproc   |            | ---------> |            |   goexit   |            |
10  |  _Gidle    | ---------> | _Grunnable |  yield     | _Grunning  | ---------> |   _Gdead   |      
11  |            |            |            | <--------- |            |            |            |
12  +------------+            +-----^------+            +------------+            +------------+
13                                  |         entersyscall |      ^ 
14                                  |                      V      | existsyscall
15                                  |                   +------------+
16                                  |   existsyscall    |            |
17                                  +------------------ |  _Gsyscall |
18                                                      |            |
19                                                      +------------+
P的状态
_Pidle: 空闲状态,未与M绑定
_Prunning: 正在运行,已经与M绑定,M 正在执行P中G;
_Psyscall: 正在执行的G处于系统调用中;
_Pgcstop: runtime正在gc;
_Pdead: 当前P已经不再使用;
状态转换图(引自 goroutine调度)如下
 1                                            acquirep(p)        
 2                          不需要使用的P       P和M绑定的时候       进入系统调用       procresize()
 3new(p)  -----+        +---------------+     +-----------+     +------------+    +----------+
 4            |         |               |     |           |     |            |    |          |
 5            |   +------------+    +---v--------+    +---v--------+    +----v-------+    +--v---------+
 6            +-->|  _Pgcstop  |    |    _Pidle  |    |  _Prunning |    |  _Psyscall |    |   _Pdead   |
 7                +------^-----+    +--------^---+    +--------^---+    +------------+    +------------+
 8                       |            |     |            |     |            |
 9                       +------------+     +------------+     +------------+
10                           GC结束            releasep()        退出系统调用
11                                            P和M解绑  
抢占
在golang程序启动时,会创建一个M(并没有关联P)来执行监控函数即sysmon,该函数就是用来完成抢占的;
- 该函数每次执行之间都会休眠一定的时间,休眠时间计算规则与每次是否抢占成功有关系:
1.1 如果连续未抢占成功的次数小于等于50,则每次休眠20us;
1.2 如果连续未抢占成功的次数大于50,则每次休眠次数翻倍;
1.3 最大休眠时间不得超过10ms; - 遍历所有的P,查看P的状态:
2.1 如果状态为_Psyscall(处于系统调用中)且执行时间已经超过了一个sysmon时间(最少20us),则进行抢占;
2.2 如果状态为_Prunning且执行时间已经超过了forcePreemptNS(10ms),则进行抢占; 
阻塞/唤醒
channel阻塞...
系统阻塞...
参考
https://studygolang.com/articles/20991
https://studygolang.com/articles/11627
https://mp.weixin.qq.com/s/Oos-aW1_khTO084v0jPlIA
https://blog.csdn.net/u010853261/article/details/84790392
go夜读 golang 调度