
 Go 调度器调度 11 种场景过程全解析

<!--more-->

### 场景 1

![image-20220117165733648](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117165733648.png)

G1 在运行中想要创建一个 G（G3），GMP 调度器会满足局部性，G1 创建的 G3 最好位于同一个 M 中。

G1 和 G3 可能会共享内存资源和会话是一致的。

G3 优先加入到 G1 所在的本地队列。

![image-20220117170337479](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117170337479.png)

### 场景 2

![image-20220117170449549](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117170449549.png)

G1 运行完成后 (函数：`goexit`)，M 上运行的 goroutine 切换为 G0（每一个 M 都有一个 G0），G0 负责调度时协程的切换（函数：`schedule`）。

> 这里的 G0 是 M 在创建时有一个结构体的指针指向 G0

![image-20220117170648802](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117170648802.png)

由于本地队列中还有 G，所以从 P 的本地队列取 G2，从 G0 切换到 G2，并开始运行 G2(函数：`execute`)。实现了线程 M1 的复用。

![image-20220117170819770](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117170819770.png)

### 场景 3

![image-20220117171056172](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117171056172.png)

有这么一个场景，假设 P1 的本地队列最多只存 4 个 G，现在 G2 创建了 6 个 G，调度器该如何处理。

首先 G2 会创建 4 个 G 将 P1 的本地队列填满

![image-20220117171722951](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117171722951.png)

接下来请看场景 4

### 场景 4

G2 在创建 G7 的时候，发现 P1 的本地队列已满，需要执行**负载均衡**(把 P1 中本地队列中前一半的 G，还有新创建 G**转移**到全局队列)，将 G3G4 打乱之后放在全局队列中，同时将新创建的 G7 也加入到全局队列中，这些 G 被转移到全局队列时，会被打乱顺序。所以 G3,G4,G7 被转移到全局队列。

![image-20220117171852942](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117171852942.png)

全局队列中的 G 大概率会被不同核运行，打乱顺序能降低不同核同时修改同一缓存行的概率

### 场景 5

G2 创建 G8 时，P1 的本地队列未满，所以 G8 会被加入到 P1 的本地队列。

![image-20220117172720440](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117172720440.png)

G8 加入到 P1 点本地队列的原因还是因为 P1 此时在与 M1 绑定，而 G2 此时是 M1 在执行。所以 G2 创建的新的 G 会优先放置到自己的 M 绑定的 P 上。

### 场景 6

规定：**在创建 G 时，运行的 G 会尝试唤醒其他空闲的 P 和 M 组合去执行**。

![image-20220117172913328](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117172913328.png)

![image-20220117173306017](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117173306017.png)

假定 G2 唤醒了 M2，M2 绑定了 P2，并运行 G0，但 P2 本地队列没有 G，M2 此时为自旋线程**（没有 G 但为运行状态的线程，不断寻找 G）**。M2 就会从先全局拿，如果全局队列中没有就去其他的本地队列去偷。

> 自旋线程：如果线程被销毁会消耗大量的硬件资源，与其让它销毁，不会让它寻找执行。自旋线程优先从全局队列中拿 G

### 场景 7

M2 尝试从全局队列 (简称“GQ”) 取一批 G 放到 P2 的本地队列（函数：`findrunnable()`）。M2 从全局队列取的 G 数量符合下面的公式：

```go
n =  min(len(GQ) / GOMAXPROCS +  1,  cap(LQ) / 2 )
```

相关源码参考：

```go
// 从全局队列中偷取，调用时必须锁住调度器
func globrunqget(_p_ *p, max int32) *g {
	// 如果全局队列中没有 g 直接返回
	if sched.runqsize == 0 {
		return nil
	}

	// per-P 的部分，如果只有一个 P 的全部取
	n := sched.runqsize/gomaxprocs + 1
	if n > sched.runqsize {
		n = sched.runqsize
	}

	// 不能超过取的最大个数
	if max > 0 && n > max {
		n = max
	}

	// 计算能不能在本地队列中放下 n 个
	if n > int32(len(_p_.runq))/2 {
		n = int32(len(_p_.runq)) / 2
	}

	// 修改本地队列的剩余空间
	sched.runqsize -= n
	// 拿到全局队列队头 g
	gp := sched.runq.pop()
	// 计数
	n--

	// 继续取剩下的 n-1 个全局队列放入本地队列
	for ; n > 0; n-- {
		gp1 := sched.runq.pop()
		runqput(_p_, gp1, false)
	}
	return gp
}
```

至少从全局队列取 1 个 g，但每次不要从全局队列移动太多的 g 到 p 本地队列，给其他 p 留点。这是**从全局队列到 P 本地队列的负载均衡**。

![image-20220117174321932](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117174321932.png)

### 场景 8

假设 G2 一直在 M1 上运行，经过 2 轮后，M2 已经把 G7、G4 从全局队列获取到了 P2 的本地队列并完成运行，全局队列和 P2 的本地队列都空了，如场景 8 图的左半部分。

![image-20220117174735759](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117174735759.png)

**全局队列已经没有 G，那 m 就要执行 work stealing(偷取)：从其他有 G 的 P 哪里偷取一半 G 过来，放到自己的 P 本地队列**。P2 从 P1 的本地队列尾部取一半的 G，本例中一半则只有 1 个 G8，放到 P2 的本地队列并执行。

### 场景 9

G1 本地队列 G5、G6 已经被其他 M 偷走并运行完成，当前 M1 和 M2 分别在运行 G2 和 G8，M3 和 M4 没有 goroutine 可以运行，M3 和 M4 处于**自旋状态**，它们不断寻找 goroutine。

![image-20220117174904551](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117174904551.png)

 为什么要让 m3 和 m4 自旋，自旋本质是在运行，线程在运行却没有执行 G，就变成了浪费 CPU. 为什么不销毁现场，来节约 CPU 资源。因为创建和销毁 CPU 也会浪费时间，我们**希望当有新 goroutine 创建时，立刻能有 M 运行它**，如果销毁再新建就增加了时延，降低了效率。当然也考虑了过多的自旋线程是浪费 CPU，所以系统中最多有`GOMAXPROCS`个自旋的线程 (当前例子中的`GOMAXPROCS`=4，所以一共 4 个 P)，多余的没事做线程会让他们休眠。

### 场景 10

假定当前除了 M3 和 M4 为自旋线程，还有 M5 和 M6 为空闲的线程 (没有得到 P 的绑定，注意我们这里最多就只能够存在 4 个 P，所以 P 的数量应该永远是 M>=P, 大部分都是 M 在抢占需要运行的 P)，G8 创建了 G9，G8 进行了**阻塞的系统调用**，M2 和 P2 立即解绑，P2 会执行以下判断：如果 P2 本地队列有 G、全局队列有 G 或有空闲的 M，P2 都会立马唤醒 1 个 M 和它绑定，否则 P2 则会加入到空闲 P 列表，等待 M 来获取可用的 p。本场景中，P2 本地队列有 G9，可以和其他空闲的线程 M5 绑定。

![image-20220117175307635](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117175307635.png)

### 场景 11

G8 创建了 G9，假如 G8 进行了**非阻塞系统调用**。

![image-20220117175619240](https://jimyag.com/posts/gogmp-go-gmp-11/index/image-20220117175619240.png)

 M2 和 P2 会解绑，但 M2 会记住 P2，然后 G8 和 M2 进入**系统调用**状态。当 G8 和 M2 退出系统调用时，会尝试获取 P2，如果无法获取，则获取空闲的 P，如果依然没有，G8 会被记为可运行状态，并加入到全局队列，M2 因为没有 P 的绑定而变成休眠状态 (长时间休眠等待 GC 回收销毁)。

### 参考

[Golang 深入理解 GPM 模型_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV19r4y1w7Nx)

