5.6 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-06-07 16:00 |
GMP 调度原理 — 概览
概述
本文档是 Go GMP 调度模型的入门篇章,从宏观架构角度回答三个核心问题:Go 为什么不用原生 OS 线程做并发?G/M/P 三个角色各自做什么?它们之间如何协作完成工作窃取、负载均衡和 IO 轮询?
读完本节后,你将建立起 GMP 的全局视图,后续章节将深入源码层面拆解每个组件的数据结构和调度流程。
正文
1.1 从线程到协程
在理解 goroutine 之前,需要先区分两个基础概念:线程(Thread) 和 协程(Coroutine)。
graph LR
A["内核态"] --> B["线程<br/>OS 管理<br/>切换成本高"]
C["用户态"] --> D["协程<br/>自我管理<br/>切换成本极低"]
style A fill:#fce4ec
style C fill:#e8f5e9
style B fill:#fff9c4
style D fill:#e3f2fd
- 线程:操作系统内核管理的执行单元,每次上下文切换需要陷入内核态(保存寄存器、切换页表等),开销较大但稳定性高。
- 协程:运行在用户态的轻量级执行流,由应用程序自主调度,切换仅需修改栈指针,开销极小。
结论:线程重而稳,协程轻而快。
1.2 Go 的答案:goroutine
Go 没有直接采用传统协程模型,而是在其之上构建了 GMP 调度体系,使 goroutine 获得两大核心优势:
- 灵活调度:G(goroutine)、M(machine,OS 线程)、P(processor,逻辑处理器)三者可动态绑定和解绑,支持水平伸缩。
- 动态栈:初始栈仅 2~8KB,随使用量自动扩容或收缩,兼顾便利性和内存利用率。
Go 在顶层屏蔽了系统线程的概念——开发者只与 goroutine 打交道,调度细节全部由 runtime 接管。
1.3 GMP 架构全景
| 角色 | 职责 | 类比(工厂) |
|---|---|---|
| G | 任务单元,包含执行栈、状态、要执行的函数体 | 工作任务 |
| M | OS 线程封装,真正执行代码的"工人" | 工人 |
| P | 调度器,管理本地队列,协调 M 与 G 的关系 | 车间主管 |
graph TB
subgraph P0["P0"]
LRQ0["LRQ: G5 → G6 → G7"]
end
subgraph P1["P1"]
LRQ1["LRQ: G8"]
end
GRQ["GRQ 全局队列<br/>[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 的执行顺序):
flowchart TD
A["LRQ<br/>无锁 CAS,最快"] -->|"空"| B{"GRQ<br/>需加锁"}
B -->|"空"| C["netpoll IO 就绪<br/>非阻塞 epoll_wait"]
B -->|"有"| E["找到 G ✓"]
C -->|"有"| E
C -->|"空"| D["work-stealing<br/>偷其他 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 取,完全无锁。
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 基础概念