Files
cs-note/hzh/GolangStar/Go语言原理/gmp调度原理/gmp-overview.md
T

148 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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["线程<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 获得两大核心优势:
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 全局队列<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` 的执行顺序):
```mermaid
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` 取,完全无锁。
```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 基础概念