169 lines
5.6 KiB
Markdown
169 lines
5.6 KiB
Markdown
---
|
||
tags: [go, golang, runtime, GMP, goroutine, scheduler]
|
||
create time: 2026-04-24 10:00
|
||
---
|
||
|
||
# GMP 调度模型
|
||
|
||
## 概述
|
||
|
||
Go 的并发模型建立在 **GMP 调度架构**之上。与传统的 OS 线程 1:1 模型不同,Go 引入了 G(Goroutine)、M(Machine/Worker)、P(Processor)三层抽象,实现了高效的**用户态调度**。
|
||
|
||
> **思考**:如果 Go 直接使用 OS 线程(1:1 模型),同时启动百万级 goroutine 会发生什么?内存开销和上下文切换代价分别有多大?
|
||
|
||
GMP 的核心目标是:**用最小的调度开销,最大化多核 CPU 的并行利用率**。
|
||
|
||
## G — Goroutine(协程)
|
||
|
||
G 是 Go 并发执行的最小单元——协程。它的核心特征:
|
||
|
||
- **初始栈空间 2KB**,可动态伸缩(最大可达 1GB)
|
||
- 栈的伸缩采用**分段扩展**策略(segment),每次增长时翻倍
|
||
- 每个 G 包含:程序计数器(PC)、寄存器状态、栈指针、等待队列指针
|
||
|
||
```go
|
||
// 创建 goroutine 的代价极低
|
||
go func() {
|
||
// 这个匿名函数运行在一个独立的协程中
|
||
fmt.Println("running in goroutine")
|
||
}()
|
||
|
||
// 对比:创建 OS 线程的代价
|
||
// runtime/debug.SetThreadThreadsLimit() 限制线程数
|
||
// 通常 OS 线程栈默认 1~8MB,且不可动态伸缩
|
||
```
|
||
|
||
> **为什么 2KB 起步?** 大多数函数不需要很大的栈空间。小默认值让创建海量协程成为可能。
|
||
|
||
## M — Machine(执行器)
|
||
|
||
M 代表**工作线程**,绑定一个 OS 线程,真正执行 G 的代码。
|
||
|
||
- **M : OS Thread = 1 : 1**,M 是 G 的执行载体
|
||
- M 负责执行 G 的代码,同时参与 GMP 调度循环
|
||
- 当 G 阻塞(如网络 IO、channel 操作)时,M 会被 P 释放去执行其他 G
|
||
|
||
```go
|
||
// 限制 Go 使用的 OS 线程数,理解 M 的规模
|
||
// GOPTHREADS=1000 go run main.go
|
||
|
||
// runtime.NumGoroutine() 返回 goroutine 数量
|
||
// runtime.NumCPU() 返回逻辑 CPU 数量
|
||
// M 的数量默认是 GOMAXPROCS(默认 NumCPU)
|
||
```
|
||
|
||
## P — Processor(处理器)
|
||
|
||
P 是**调度器本地状态**的抽象,可以理解为一个"局部调度器"。
|
||
|
||
- P 维护一个**本地 goroutine 队列**(runq),最多缓存 256 个待执行的 G
|
||
- P 的数量由 `GOMAXPROCS` 控制(Go 1.5+ 默认等于 CPU 逻辑核心数)
|
||
- **每个时刻,一个 P 只能绑定一个 M** 来执行代码
|
||
|
||
P 的核心职责:
|
||
|
||
| 职责 | 说明 |
|
||
|------|------|
|
||
| 本地调度 | 从自己的 runq 取出 G 分配给绑定的 M 执行 |
|
||
| 工作窃取(Work Stealing)| 当自己的队列为空时,从其他 P 的队列中"偷"一半的 G |
|
||
| 全局队列 | 所有 P 共享一个全局 G 队列(最多容纳 64 个),用于负载均衡 |
|
||
| 系统调用处理 | 当绑定的 M 进入系统调用时,P 会"脱落"(handoff),找一个空闲 M 绑定 |
|
||
|
||
## GMP 调度循环
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph GQueue["G 队列"]
|
||
G1["G1 待执行"]
|
||
G2["G2 待执行"]
|
||
G3["G3 待执行"]
|
||
G4["G4 待执行"]
|
||
G5["G5 待执行"]
|
||
end
|
||
|
||
subgraph GlobalQ["全局队列 (最多64个)"]
|
||
GQ["Global run queue"]
|
||
end
|
||
|
||
subgraph LocalQ["P 本地队列 (最多256个)"]
|
||
PQL["P local runq"]
|
||
end
|
||
|
||
subgraph MExec["M 执行 G"]
|
||
ME["执行 G 的代码"]
|
||
end
|
||
|
||
subgraph OSys["操作系统"]
|
||
NET["网络 IO / 系统调用"]
|
||
SYS["系统调用 - 阻塞"]
|
||
end
|
||
|
||
GQueue --> |入队| GlobalQ
|
||
GlobalQ --> |P 空闲时取| PQL
|
||
PQL --> |分配给 M| ME
|
||
ME --> |执行完 G| ME
|
||
ME --> |新建 G| PQL
|
||
ME --> |阻塞 channel/IO | NET
|
||
ME --> |系统调用| SYS
|
||
|
||
SYS --> |P 脱落, 另找 M| PQL
|
||
NET --> |完成阻塞| PQL
|
||
|
||
PQL --> |队列太长, 移到全局| GQ
|
||
GQ --> |工作窃取: 从其他 P 偷一半| PQL
|
||
|
||
classDef queue fill:#e3f2fd,stroke:#1565c0
|
||
classDef exec fill:#fff3e0,stroke:#e65100
|
||
classDef sys fill:#fce4ec,stroke:#c62828
|
||
class GlobalQ,PQL queue
|
||
class ME exec
|
||
class NET,SYS sys
|
||
```
|
||
|
||
**调度流程**:
|
||
|
||
1. **新建 G** → 先尝试放入 P 的本地队列(`runq`),满了则放入全局队列
|
||
2. **P 取 G** → 从本地队列取(优先),本地为空时从全局队列取,再空则"工作窃取"其他 P
|
||
3. **M 执行 G** → M 拿着 P 分配的 G 执行代码
|
||
4. **G 阻塞** → G 进入等待队列,M 继续执行下一个 G,P 寻找新的 M 绑定
|
||
5. **G 恢复** → G 被放回到某个 P 的队列中,等待下次被调度
|
||
|
||
## 为什么不用 M:N 模型?
|
||
|
||
早期 Go 讨论过 M:N 模型(N 个 G 映射到 M 个 OS 线程),最终选择了 G:M ≈ 1:1 + P 用户态调度,原因:
|
||
|
||
| 对比维度 | M:N 模型 | Go 的 GMP 模型 |
|
||
|----------|----------|----------------|
|
||
| 系统调用阻塞 | M 被阻塞,N 个 G 全部卡住 | G 阻塞,M 脱手,P 找新 M |
|
||
| 抢占式调度 | 需要内核协作 | 用户态即可,P 完全控制 |
|
||
| 实现复杂度 | 极高(需要内核干预) | 相对简洁 |
|
||
| Go 版本 | 1.1 之前实验过 | 1.1+ 确定方案 |
|
||
|
||
> **关键点**:Go 1.5 引入 GOMAXPROCS = NumCPU,每个 P 绑定一个逻辑 CPU 核心,实现了真正的并行执行。
|
||
|
||
## 关键参数
|
||
|
||
```go
|
||
// 获取和设置 GOMAXPROCS
|
||
import "runtime"
|
||
|
||
func main() {
|
||
n := runtime.GOMAXPROCS(0) // 获取当前值
|
||
runtime.GOMAXPROCS(4) // 设置为 4
|
||
fmt.Println("M 的最大并行度:", n)
|
||
}
|
||
|
||
// 其他 runtime 参数
|
||
fmt.Println("Goroutine 数量:", runtime.NumGoroutine())
|
||
fmt.Println("逻辑 CPU 数量:", runtime.NumCPU())
|
||
```
|
||
|
||
> **思考**:`GOMAXPROCS=1` 时,Go 还是并行的吗?还是只是并发?
|
||
|
||
## 关联笔记
|
||
|
||
- [[DEV/GO/GC垃圾回收]]
|
||
- [[DEV/GO/Channel详解]]
|
||
- [[DEV/GO/内存分配与逃逸分析]]
|
||
- [[DEV/GO/Goroutine泄漏排查]]
|