# GMP 模型原理

> golang调度的机制用一句话描述: runtime准备好G、P、M，然后M绑定P，M从各种队列中获取G，切换到G的执行栈上并执行G上的任务函数，调用goexit做清理工作并回到M，如此反复。

### 1. 什么是 GMP ？

* `G`：Goroutine，也就是 go 里的协程，是用户态的轻量级线程，具体可以创建多个 goroutine ，取决你的内存有多大，一个 goroutine 大概需要 4k 内存，只要你不是在 32 位的机器上，那么创建个几百万个的 goroutine 应该没有问题。
* `M`：Thread，也就是操作系统线程，go runtime 最多允许创建 10000 个操作系统线程，超过了就会抛出异常
* `P`：Processor，处理器，数量默认等于开机器的cpu核心数，若想调小，可以通过 GOMAXPROCS 这个环境变量设置。

### 2. GMP 核心

#### 两个队列

在整个 Go 调度器的生命周期中，存在着两个非常重要的队列：

* 全局队列（Global Queue）：全局只有一个
* 本地队列（Local Queue）：每个 P 都会维护一个本地队列

当你执行 `go func()` 创建一个 goroutine 时，会优选将该协程放入到当前 P 的本地队列中等待被 P 选中执行。

但若当前 P 的本地队列任务太多了，已经存放不下了，那么这个 goroutine 就只能放入到全局队列中。

#### 两种调度

一个协程得以运行，需要同时满足以下两个条件：

1. P 已经和某个线程进行绑定，这样才能参与操作系统的调度获得 CPU 时间
2. P 已经从队列中（可以是本地队列，也可以是全局队列，甚至是从其他 P 的队列）取到该协程

第一个条件就是 **操作系统调度**，而第二个其实就是 **Go 里的调度器**。

**操作系统调度**

假设一台机器上有两个 CPU 核心，意味着，同时在同一时间里，只能有两个线程运行着。

可如果该机器上实际开启了 4 个线程，要是先执行一个线程完再执行另一个线程，那么当某一个线程因为一些阻塞性的系统调用而阻塞时，CPU 的时间就会因此而白白浪费掉了。

更合适的做法是，使用 **操作系统调度策略**，设定一个调度周期，假设是 10ms （毫秒），那在一个周期里，每个线程都平均分，都只能得到 2.5ms 的CPU 运行时间。

可如果机器上有 1000 个线程呢？难道每个线程都分个 0.01 ms （也就是 10 微秒）吗？

要知道，CPU 从 A 线程切换到 B 线程，是有巨大的时间浪费在线程上下文的切换，如果切换得太频繁，就会有大量的 CPU 时间白白浪费。

![image0](http://image.iswbm.com/20210904140447.png)

因此，通常会限制最小的时间片的长度，假设为 2ms，受此调整，现在调度周期就会变成 2\*1000 = 2s 。

**Go调度器**

在 Go 中需要用到调度的，无非是如下几种：

**将 P 绑定到一个合适的 M**

P 本身不能直接运行 G，只将 P 跟 M 绑定后，才能执行 G。

假设 P1 当前正绑定在 M1 上运行 G1，此时 G1 内部发生了一次系统调度后，P1 就会与 M1 进行解绑，然后再从空闲的线程队列中再寻找一个来绑定，假设绑定的是 M2，可如果没有空闲的线程呢？那没办法，只能创建一个新的线程再进行绑定。

绑定后，就会再从本地的队列中寻找 G 来执行（如果没找到，就会去其他队列找，上面已经讲过，不再赘述）。

过了一段时间后，之前 M1 上 G1 发生的系统调用结束后，M1 会去找原先自己的搭档 P1（它自己会记录），如果自己的老搭档也刚好空闲着，就可以再次合作进行绑定，接着运行 G1 未完成的工作。

可不幸的是，P1 已经找到了新的合作伙伴 M2，暂时没空搭理 M1 。

M1 联系不上 P1，只能去寻找有没有其他空闲的 P ，如果所有的 P 都被绑定着，说明现在任务非常繁重，想完成任务只能排队慢慢等。

于是，M1 上的 G1 就会被标记为 Runable ，放到全局队列中，而 M1 自身也会因为没有 P 可以绑定而进入休眠状态，如果长时间休眠等待 则会 GC 回收销毁

**为 P 选中一个 G 来执行**

P 就像是一个流水线工人，而 P 的本地队列就是流水线，G 是流水线上的零件。而 Go 调度器就是流水线组长，负责监督工人的是不是有在努力的工作。

完成一个 G 后，P 就得立马接着从队列中拿到下一个 G，继续干活。

遇到手脚麻利的 P ，干完了自己的活，本想着可以偷懒一会，没想到却被组长发现了，立马就从全局队列中拿了些新的 G 交到 P 的手里。

天真的 P 以为只要把 全局队列中的 G 的也干完了，就肯定 能休息了吧？

当 P 又快手快脚的把全局队列中的 G 也都干完的时候，P 非常得意，心想：终于可以休息会了。

没想到又被眼尖的组长察觉到了：不错啊，小 P，手脚挺麻利的。看看其他人，还那么多活没干完。真是拖后腿。可谁让咱是一个团队的呢，要有集体荣誉感，你能者多劳。

说完，就把其他人的 G 放到了我的工作上。。。

### 3. 调度器的设计策略

#### 复用线程

避免频繁的创建、销毁线程，而是对线程的复用。

**1）work stealing 机制**

当本线程无可运行的 G 时，尝试从其他线程绑定的 P 偷取 G，而不是销毁线程。

**2）hand off 机制**

当本线程因为 G 进行系统调用阻塞时，线程释放绑定的 P，把 P 转移给其他空闲的线程执行。

#### 利用并行

GOMAXPROCS 设置 P 的数量，最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度，比如 GOMAXPROCS = 核数/2，则最多利用了一半的 CPU 核进行并行。

#### 抢占调度

在 Go 中，一个 goroutine 最多占用 CPU 10ms，防止其他 goroutine 被饿死，这是协作式抢占调度。

而在 go 1.14+ ，Go 开始支持基于信号的真抢占调度了。

#### 全局 G 队列

在新的调度器中依然有全局 G 队列，但功能已经被弱化了，当 M 执行 work stealing 从其他 P 偷不到 G 时，它可以从全局 G 队列获取 G。

### 延伸阅读

* [\[典藏版\] Golang 调度器 GMP 原理与调度全分析](https://learnku.com/articles/41728)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://go.codetrue.net/concurrent-programming/goroutine-scheduling.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
