vault backup: 2026-06-07 12:14:39
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go原理深入
|
||||
- GMP调度
|
||||
- 协程调度
|
||||
tags: [go, golang, go-principle, gmp-scheduler]
|
||||
create time: 2026-06-07 15:50
|
||||
---
|
||||
|
||||
# GMP调度原理
|
||||
# GMP 调度原理
|
||||
|
||||
## 概述
|
||||
|
||||
本文从宏观架构到微观源码,全方位解析 Go 的 GMP 调度模型。涵盖 G/M/P 三大组件的数据结构、goroutine 的创建与调度流程、主动让渡机制(yield)、以及 sysmon 线程发起的抢占式调度。这是理解 Go 并发性能的核心篇章。
|
||||
|
||||
> [!question] ❓ 思考
|
||||
> 为什么 Go 不直接使用操作系统线程来管理并发,而要发明 GMP 这套中间层?当一个 goroutine 阻塞在 IO 上时,整个程序的其他 goroutine 还会继续执行吗?
|
||||
|
||||
---
|
||||
|
||||
聊到Go语言,大家最津津乐道的可能就是它那"天生强大"的并发能力了。一个简单的 `go` 关键字,就能开启一个并发执行单元,这酸爽,谁用谁知道。但是,你有没有想过,这背后到底藏着什么样的魔法?为什么Go的并发可以如此轻盈、如此高效?
|
||||
|
||||
|
||||
答案,就藏在它核心的 **GMP调度模型**里。
|
||||
|
||||
很多Gopher对GMP可能只是略知一二,知道有G、M、P这三个角色,但它们之间是如何协作的,一个goroutine又是如何被创建、调度、甚至是被"抢占"的,可能就有点模糊了。
|
||||
@@ -70,29 +75,72 @@ Go语言选择的并发实现,就是我们所熟知的 **goroutine**。你可
|
||||
|
||||
* **P(Processor)**:P是调度器,是GMP模型中的"中枢大脑"。M必须获取到一个P,才能开始调度和执行G。P的数量决定了同一时间最多有多少个M可以处于运行状态,这个数量通常由 `GOMAXPROCS` 环境变量决定。P还有一个非常重要的职责:它自带一个本地的goroutine队列,我们称之为 **LRQ (Local Run Queue)**。
|
||||
|
||||
G、M、P 三者的关系可以用一张图来概括:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph P1["P0 (Processor)"]
|
||||
LRQ0["LRQ: [G5, G6, G7]"]
|
||||
end
|
||||
subgraph P2["P1 (Processor)"]
|
||||
LRQ1["LRQ: [G8]"]
|
||||
end
|
||||
GRQ["GRQ 全局队列<br/>[G1, G2, G3, G4]"]
|
||||
M0["M0 (OS Thread)"] -->|绑定| P0_node["P0"]
|
||||
M1["M1 (OS Thread)"] -->|绑定| P1_node["P1"]
|
||||
P0_node --> LRQ0
|
||||
P1_node --> LRQ1
|
||||
LRQ0 -.偷取.-> LRQ1
|
||||
LRQ1 -.偷取.-> LRQ0
|
||||
GRQ -->|LRQ空时取| P0_node
|
||||
GRQ -->|LRQ空时取| P1_node
|
||||
|
||||
style GRQ fill:#fff9c4
|
||||
style LRQ0 fill:#e8f5e9
|
||||
style LRQ1 fill:#e8f5e9
|
||||
```
|
||||
|
||||
> [!tip] 💡 类比理解
|
||||
> 把 GMP 想象成一个工厂:
|
||||
> - **G** = 工作任务(如"组装一台手机")
|
||||
> - **M** = 工人(真正干活的 OS 线程)
|
||||
> - **P** = 车间主管(管理工人、分配任务)
|
||||
> - **LRQ** = 每个主管面前的待办清单
|
||||
> - **GRQ** = 工厂公共公告栏上的大清单
|
||||
> - **work-stealing** = 闲着的工人去隔壁忙不过来的车间"偷"活干
|
||||
|
||||

|
||||
|
||||
现在,我们把目光聚焦到存放G的"容器"上。Go的设计非常巧妙,它有两种队列:
|
||||
|
||||
1. **P的本地队列(LRQ - Local Run Queue)**:这是每个P私有的G队列。当一个M想执行G时,会优先从自己绑定的P的LRQ里找。因为是私有的,所以大部分时间不需要加锁,通过高效的CAS(Compare-And-Swap)操作就能完成存取,大大减少了并发冲突。当然,也并非完全没有冲突,因为当一个P的LRQ空了的时候,它可能会从其他P的LRQ里"偷"一些G过来,这就是著名的 **work-stealing** 机制。
|
||||
1. **P 的本地队列(LRQ - Local Run Queue)**:每个 P 私有的 G 队列,最多存 256 个 G。优先无锁 CAS 操作存取,极少加锁。当 LRQ 满了或需要负载均衡时,会触发 **work-stealing** 机制——从其他 P 的 LRQ 中"偷"一半过来。
|
||||
|
||||
2. **全局队列(GRQ - Global Run Queue)**:这是一个全局共享的G队列。当一个P的LRQ满了,新创建的G就会被放到GRQ里。因为是全局共享的,所以所有M都可能来访问,竞争激烈,因此访问它需要加一把全局大锁。
|
||||
2. **全局队列(GRQ - Global Run Queue)**:所有 M 共享的 G 队列,访问需加全局锁 `sched.lock`。新创建的 G 在 LRQ 满时被放入 GRQ。
|
||||
|
||||
**G的存放与获取逻辑**:
|
||||
**G 的存放与获取逻辑**:
|
||||
|
||||
* **放G(put g)**:当你在一个goroutine里通过 `go func(){...}` 创建一个新的goroutine时,它会优先被放到当前P的LRQ里。如果LRQ满了,没办法,只能加个全局锁,把它扔到GRQ里去。这遵循的是"就近原则"。
|
||||
* **放 G(put g)**:`go func(){...}` 创建的新 goroutine 优先放入当前 P 的 LRQ。LRQ 满了才加锁放入 GRQ。遵循"就近原则"。
|
||||
|
||||
* **取G(get g)**:当M上的`g0`开始找活干时,它会遵循一个"负载均衡"的策略,按以下顺序来寻找G:
|
||||
* **取 G(get g)**:M 上的 `g0` 找任务时按以下优先级:
|
||||
|
||||
1. 先从当前P的LRQ里找(无锁,速度最快)。
|
||||
```mermaid
|
||||
flowchart TD
|
||||
LRQ["1. 当前 P 的 LRQ<br/>无锁 CAS,最快"] -->|空| GRQ{"2. 全局 GRQ<br/>需加锁"}
|
||||
GRQ -->|空| NET{"3. netpoll IO就绪<br/>非阻塞 epoll_wait"}
|
||||
GRQ -->|有| GOT_G["找到 G ✓"]
|
||||
NET -->|有| GOT_G
|
||||
NET -->|空| STEAL{"4. 从其他 P 偷一半<br/>work-stealing"}
|
||||
STEAL -->|成功| GOT_G
|
||||
STEAL -->|失败| SLEEP["5. P/M 进入休眠"]
|
||||
style LRQ fill:#e8f5e9
|
||||
style GRQ fill:#fff3e0
|
||||
style NET fill:#fff9c4
|
||||
style STEAL fill:#ffebee
|
||||
style SLEEP fill:#eceff1
|
||||
```
|
||||
|
||||
2. 如果LRQ没有,就去全局GRQ里看看(需要加锁)。
|
||||
|
||||
3. 如果GRQ也没有,就去网络轮询器(netpoll)里找找有没有因为IO操作而就绪的G。
|
||||
|
||||
4. 如果还是没有,就只能去"偷"了,从别的P的LRQ里偷一半过来(work-stealing,无锁)。
|
||||
|
||||
这里有个小细节:为了防止GRQ里的G被"饿死"(因为M上的`g0`总是优先从LRQ取),调度器规定,每进行61次调度循环,就必须强制去下一次去`grq`取 。这样做是为了避免`lrq`过于繁忙,而导致`grq`中的`g`"饿死"。
|
||||
> [!note] 📝 防饥饿机制
|
||||
> 为了防止 GRQ 中的 G 被长期饿死(因为 M 总是优先从 LRQ 取),调度器规定:**每进行 61 次调度循环,就必须强制去 GRQ 取一次**。这保证了公平性。
|
||||
|
||||
### 1.4 GMP生态圈
|
||||
|
||||
@@ -1402,5 +1450,68 @@ func asyncPreempt2() {
|
||||
|
||||
希望通过这篇文章,你能对Go的并发调度有一个更深刻、更系统的理解。GMP模型无疑是Go语言设计的精髓所在,它优雅、高效地解决了并发调度中的种种难题,是我们每个Gopher都应该掌握的核心知识。
|
||||
|
||||
---
|
||||
|
||||
### GMP 全景回顾
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Create["创建 goroutine"]
|
||||
NewProc["runtime.newproc<br/>systemstack → newproc1<br/>runqput → LRQ/GRQ"]
|
||||
end
|
||||
|
||||
subgraph Schedule["调度循环"]
|
||||
ScheduleFn["schedule() → findRunnable()"]
|
||||
FindR["findRunnable: LRQ→GRQ→netpoll→steal"]
|
||||
Execute["execute() → gogo(g)"]
|
||||
end
|
||||
|
||||
subgraph Yield["让出执行权"]
|
||||
End["goexit1 → mcall(goexit0)<br/>status=Gdead, gfput"]
|
||||
Gosched["Gosched → mcall(gosched_m)<br/>status=Grunnable, globrunqput"]
|
||||
Park["gopark → mcall(park_m)<br/>status=Gwaiting, 由上层管理"]
|
||||
end
|
||||
|
||||
subgraph Preempt["抢占 (sysmon)"]
|
||||
Sysmon["sysmon 线程"]
|
||||
Retake["retake: 系统调用 + 超时检测"]
|
||||
Collab["协作式: stackPreempt 标记"]
|
||||
Signal["非协作式: sigPreempt 信号注入"]
|
||||
end
|
||||
|
||||
subgraph Recover["恢复执行"]
|
||||
Ready["goready → ready()<br/>status=Grunnable, runqput"]
|
||||
end
|
||||
|
||||
Create --> Schedule
|
||||
Schedule --> Yield
|
||||
Yield --> Recover
|
||||
Recover --> Schedule
|
||||
Sysmon --> Preempt
|
||||
Preempt --> Yield
|
||||
style Create fill:#e3f2fd
|
||||
style Schedule fill:#e8f5e9
|
||||
style Yield fill:#fff3e0
|
||||
style Preempt fill:#ffebee
|
||||
style Recover fill:#e8f5e9
|
||||
```
|
||||
|
||||
> [!note] 📝 核心要点总结
|
||||
> - **G** = 任务(goroutine),有自己的栈和执行状态
|
||||
> - **M** = 工人(OS 线程),真正执行 G 的代码
|
||||
> - **P** = 主管(逻辑处理器),管理 LRQ 并协调 M 与 G 的关系
|
||||
> - 调度优先级:LRQ(无锁)> GRQ(加锁)> netpoll(IO)> steal(工作窃取)
|
||||
> - 防饥饿:每 61 次调度强制检查 GRQ
|
||||
> - 抢占机制:协作式(栈检查标记)+ 非协作式(信号注入)+ 系统调用退出抢占
|
||||
> - sysmon 是永不休息的"巡逻兵",负责 IO 轮询、抢占检查和 GC 触发
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hzh/GolangStar/Go语言进阶/Goroutine]] — Goroutine 的基础概念
|
||||
- [[hzh/GolangStar/Go语言基础/Go语言函数]] — 函数的返回值与 defer
|
||||
- [[hzh/GolangStar/Go语言原理/内存管理]] — GMP 与内存管理的协作
|
||||
- [[hzh/GolangStar/Go语言原理/垃圾回收]] — sysmon 如何触发 GC
|
||||
- [[hzh/GolangStar/Go面试题库/GMP面试题]] — GMP 调度相关高频面试题
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user