--- 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 基础概念