---
tags: [go, golang, go-principle, gmp-scheduler, overview]
create time: 2026-06-07 16:00
---
# GMP 调度原理 — 概览
## 概述
本文档是 Go GMP 调度模型的入门篇章,从宏观架构角度回答三个核心问题:Go 为什么不用原生 OS 线程做并发?G/M/P 三个角色各自做什么?它们之间如何协作完成工作窃取、负载均衡和 IO 轮询?
读完本节后,你将建立起 GMP 的全局视图,后续章节将深入源码层面拆解每个组件的数据结构和调度流程。
## 正文
### 1.1 从线程到协程
在理解 goroutine 之前,需要先区分两个基础概念:**线程(Thread)** 和 **协程(Coroutine)**。
```mermaid
graph LR
A["内核态"] --> B["线程
OS 管理
切换成本高"]
C["用户态"] --> D["协程
自我管理
切换成本极低"]
style A fill:#fce4ec
style C fill:#e8f5e9
style B fill:#fff9c4
style D fill:#e3f2fd
```
- **线程**:操作系统内核管理的执行单元,每次上下文切换需要陷入内核态(保存寄存器、切换页表等),开销较大但稳定性高。
- **协程**:运行在用户态的轻量级执行流,由应用程序自主调度,切换仅需修改栈指针,开销极小。
**结论**:线程重而稳,协程轻而快。
### 1.2 Go 的答案:goroutine
Go 没有直接采用传统协程模型,而是在其之上构建了 **GMP 调度体系**,使 goroutine 获得两大核心优势:
1. **灵活调度**:G(goroutine)、M(machine,OS 线程)、P(processor,逻辑处理器)三者可动态绑定和解绑,支持水平伸缩。
2. **动态栈**:初始栈仅 2~8KB,随使用量自动扩容或收缩,兼顾便利性和内存利用率。
Go 在顶层屏蔽了系统线程的概念——开发者只与 goroutine 打交道,调度细节全部由 runtime 接管。
### 1.3 GMP 架构全景
| 角色 | 职责 | 类比(工厂) |
|------|------|-------------|
| **G** | 任务单元,包含执行栈、状态、要执行的函数体 | 工作任务 |
| **M** | OS 线程封装,真正执行代码的"工人" | 工人 |
| **P** | 调度器,管理本地队列,协调 M 与 G 的关系 | 车间主管 |
```mermaid
graph TB
subgraph P0["P0"]
LRQ0["LRQ: G5 → G6 → G7"]
end
subgraph P1["P1"]
LRQ1["LRQ: G8"]
end
GRQ["GRQ 全局队列
[G1, G2, G3, G4]"]
M0["M0"] --> P0_node["P0"]
M1["M1"] --> P1_node["P1"]
P0_node --> LRQ0
P1_node --> LRQ1
LRQ0 -.work-stealing.-> LRQ1
LRQ1 -.work-stealing.-> LRQ0
GRQ -->|"LRQ 空时取"| P0_node
GRQ -->|"LRQ 空时取"| P1_node
style GRQ fill:#fff9c4
style LRQ0 fill:#e8f5e9
style LRQ1 fill:#e8f5e9
```
#### 两种队列
G 存放在两种队列中,获取优先级由高到低:
| 队列 | 说明 | 锁 | 容量 |
|------|------|-----|------|
| **LRQ**(Local Run Queue) | 每个 P 私有的 G 队列 | 无锁 CAS | 最多 256 个 |
| **GRQ**(Global Run Queue) | 所有 P 共享的全局队列 | `sched.lock` | 理论上无限 |
**放置策略**:新创建的 G 优先放入当前 P 的 LRQ;LRQ 满时才加锁放入 GRQ。
**获取策略**(`findrunnable` 的执行顺序):
```mermaid
flowchart TD
A["LRQ
无锁 CAS,最快"] -->|"空"| B{"GRQ
需加锁"}
B -->|"空"| C["netpoll IO 就绪
非阻塞 epoll_wait"]
B -->|"有"| E["找到 G ✓"]
C -->|"有"| E
C -->|"空"| D["work-stealing
偷其他 P 的一半"]
D -->|"成功"| E
D -->|"失败"| F["P/M 进入休眠"]
style A fill:#e8f5e9
style B fill:#fff3e0
style C fill:#fff9c4
style D fill:#ffebee
style F fill:#eceff1
```
> [!note] 📝 防饥饿机制
> 为防止 GRQ 中的 G 长期饿死,每 61 次调度循环强制检查一次 GRQ,保证公平性。
### 1.4 GMP 生态圈
GMP 是 Go 运行时的心跳中枢,上层多数子系统都围绕它设计。
#### 1.4.1 内存管理
Go 借鉴 Google TCMalloc 思想,为每个 P 配备私有缓存 `mcache`。P 上的 G 分配小对象时直接从 `mcache` 取,完全无锁。
```mermaid
graph LR
G1["G₁"] -->|无锁| MC1["P₀ → mcache"]
G2["G₂"] -->|无锁| MC1
MC1 -->|"mcache 耗尽"| MH["mcentral → mspan"]
MH -->|"mspan 耗尽"| MM["mmapped 直接映射"]
style MC1 fill:#e8f5e9
style MH fill:#fff9c4
style MM fill:#fce4ec
```
#### 1.4.2 并发工具(Mutex / Channel)
Go 的同步原语是 **G 级别** 的——当 G 因等待 channel 数据或 Mutex 而阻塞时,它调用 `gopark` 主动让出 M,而不是阻塞整条 OS 线程。这使得单个 OS 线程上可以承载海量并发。
> [!question] ❓ 对比思考
> C++ 标准库锁一旦持有,阻塞的是整个线程,该线程上的所有协程都被迫等待。Go 通过 G 级别阻塞实现了更细粒度的并发控制。
#### 1.4.3 IO 多路复用(netpoll)
网络 IO 场景下,Go 使用 Linux `epoll` 作为底层轮询机制,并通过 `netpoll` 将其包装为 G 级别的异步通知:
- G 发起 IO → `gopark` 挂起自身
- OS 就绪 → netpoll 发现 → `goready` 唤醒 G
IO 操作由此无缝融入 GMP 调度环,无需额外线程。
## 关联笔记
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-datastructures]] — G/M/P/Schedt 数据结构源码剖析
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-lifecycle]] — Goroutine 创建、调度与让渡流程
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-preemption]] — sysmon 抢占式调度机制
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-summary]] — 知识卡片与全景回顾
- [[hzh/GolangStar/Go语言进阶/Goroutine]] — Goroutine 基础概念