### Go goroutine调度
- 目的：将 Goroutine 按照一定算法放到不同的操作系统线程中去执行
- 调度模型与算法几经演化，从最初的 G-M 模型、到 G-P-M 模型，从不支持抢占，到支持协作式抢占，再到支持基于信号的异步抢占

### GM模型
- 每个 Goroutine 对应于运行时中的一个抽象结构：G(Goroutine) 
- 被视作“物理 CPU”的操作系统线程，则被抽象为另外一个结构：M(machine)

#### GM不足
- 单一全局互斥锁(Sched.Lock) 和集中状态存储的存在，导致所有 Goroutine 相关操作，比如创建、重新调度等，都要上锁
- Goroutine 传递问题：M 经常在 M 之间传递“可运行”的 Goroutine，这导致调度延迟增大，也增加了额外的性能损耗
- 每个 M 都做内存缓存，导致内存占用过高，数据局部性较差
- 由于系统调用（syscall）而形成的频繁的工作线程阻塞和解除阻塞，导致额外的性能损耗
> 集中状态(centralized state)，就是一把全局锁要保护的数据太多。这样无论访问哪个数据，都要锁这把全局锁。数据局部性差是因为每个m都会缓存它执行的代码或数据，但是如果在m之间频繁传递goroutine，那么这种局部缓存的意义就没有了。无法实现局部缓存带来的性能提升。


### GMP模型
- 在 Go 1.1 版本中实现了 G-P-M 调度模型和work stealing 算法，这个模型一直沿用至今
![](https://blog-heysq-1255479807.cos.ap-beijing.myqcloud.com/blog/wiki/go/gmp.png)

    - P 是一个“逻辑 Proccessor”，每个 G（Goroutine）要想真正运行起来，首先需要被分配一个 P，也就是进入到 P 的本地运行队列（local runq）中
    - 对于 G 来说，P 就是运行它的“CPU”，可以说：在 G 的眼里只有 P
    - 从 Go 调度器的视角来看，真正的“CPU”是 M，只有将 P 和 M 绑定，才能让 P 的 runq 中的 G 真正运行起来

- Go 1.1 模型不支持抢占式调度
- GO 1.2 加入抢占式调度，原理就是，Go 编译器在每个函数或方法的入口处加上了一段额外的代码 (runtime.morestack_noctxt)，让运行时有机会在这段代码中检查是否需要执行抢占调度，弊端就是只有在函数或方法不能能加入代码，纯算法循环代码无法加入调度功能（比如死循环）
- Go 1.14 版本中增加了对非协作的抢占式调度的支持，这种抢占式调度是基于系统信号的，也就是通过向线程发送信号的方式来抢占正在运行的 Goroutine
> 协作式：大家都按事先定义好的规则来，比如：一个goroutine执行完后，退出，让出p，然后下一个goroutine被调度到p上运行。这样做的缺点就在于 是否让出p的决定权在groutine自身。一旦某个g不主动让出p或执行时间较长，那么后面的goroutine只能等着，没有方法让前者让出p，导致延迟甚至饿死。而非协作: 就是由runtime来决定一个goroutine运行多长时间，如果你不主动让出，对不起，我有手段可以抢占你，把你踢出去，让后面的goroutine进来运行
### G
- goroutine 缩写，每次go func()都代表一个G，无限制，而且 G 对象是可以重用的
- 使用struct runtime.g，包含了当前goroutine的状态，堆栈和上下文

### M
- 工作线程（OS thread）也被称为Machine，使用 struct  runtime.m，所有M是有线程栈的（1-8M）
- 执行流程是从 P 的本地运行队列以及全局队列中获取 G，切换到 G 的执行栈上并执行 G 的函数，调用 goexit 做清理工作并回到 M，如此反复
- 如果不对该线程栈提供内存的话，系统会给该线程栈提供内存(不同操作系统提供的线程栈大小不同)。当指定了线程栈，则 M.stack→G.stack，M 的 PC 寄存器指向 G 提供的函数，然后去执行

### P
- Processor是一个抽象概念，并不是真正的物理CPU
- 代表了M所需的上下文环境，也是处理用户级代码逻辑的处理器。负责衔接M和G的调度上下文，将等待执行的G和M进行连接。当P有任务是需要创建或者唤醒一个M来执行 它队列里的任务，所以P/M需要进行绑定，构成一个执行单元
- P决定了并发任务的数量，通过runtime.GOMAXPROCS来设定，Go1.5之后被默认设置为可用核数，之前默认设置为1

### GMP优化部分
- netpoller，即便 G 发起网络 I/O 操作，也不会导致 M 被阻塞（仅阻塞 G），也就不会导致大量线程（M）被创建出来
- io poller，这个功能可以像 netpoller 那样，在 G 操作那些支持监听（pollable）的文件描述符时，仅会阻塞 G，而不会阻塞 M。不支持对常规文件的监听

### 协作抢占式监控线程 sysmon M
- M 的特殊之处在于它不需要绑定 P 就可以运行（以 g0 这个 G 的形式）
- sysmon 每 20us~10ms 启动一次
- 释放闲置超过 5 分钟的 span 内存
- 如果超过 2 分钟没有垃圾回收，强制执行
- 将长时间未处理的 netpoll 结果添加到任务队列
- 向长时间运行的 G 任务发出抢占调度
- 收回因 syscall 长时间阻塞的 P

### sysmon M 抢占调度goroutine
- 如果一个 G 任务运行 10ms，sysmon 就会认为它的运行时间太久而发出抢占式调度的请求
- 一旦 G 的抢占标志位被设为 true，等到这个 G 下一次调用函数或方法时，运行时就可以将 G 抢占并移出运行状态，放入队列中，等待下一次被调度

### channel 阻塞或网络 I/O 情况下的调度
- G 被阻塞在某个 channel 操作或网络 I/O 操作上时，G 会被放置到某个等待（wait）队列中，而 M 会尝试运行 P 的下一个可运行的 G
- 如果这个时候 P 没有可运行的 G 供 M 运行，那么 M 将解绑 P，并进入挂起状态
- 当 I/O 操作完成或 channel 操作完成，在等待队列中的 G 会被唤醒，标记为可运行（runnable），并被放入到某 P 的队列中，绑定一个 M 后继续执行

### 系统调用阻塞情况下的调度
- G 被阻塞在某个系统调用（system call）上，不光 G 会阻塞，执行这个 G 的 M 也会解绑 P，与 G 一起进入挂起状态
- 如果此时有空闲的 M，那么 P 就会和它绑定，并继续执行其他 G
- 如果没有空闲的 M，但仍然有其他 G 要去执行，Go 运行时就会创建一个新 M（线程）
- 当进行一些慢系统调用的时候，比如常规文件io，执行系统调用的m就要和g一起挂起，这是os的要求，不是go runtime的要求。毕竟真正执行代码的还是m
