286 lines
9.0 KiB
Markdown
286 lines
9.0 KiB
Markdown
|
|
---
|
|||
|
|
tags: [go, golang, go-principle, gmp-scheduler, lifecycle]
|
|||
|
|
create time: 2026-06-07 16:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# GMP 调度原理 — Goroutine 生命周期
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
本节从第一人称视角追踪 goroutine 的完整生命周期:从 `go func()` 触发的创建流程,到被调度器翻牌子执行,再到主动让出执行权的三种方式。涵盖 §3(正向追踪)和 §4(逆向追踪)。
|
|||
|
|
|
|||
|
|
## 正文
|
|||
|
|
|
|||
|
|
### 3.1 main 函数的特殊性
|
|||
|
|
|
|||
|
|
`main` 函数是 Go 程序的唯一起点,由全局唯一的 `m0`(主线程)直接执行,不涉及普通的调度流程。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func main() {
|
|||
|
|
fn := main_main // 获取用户定义的 main 函数
|
|||
|
|
fn() // 直接执行
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 普通 G 的创建流程
|
|||
|
|
|
|||
|
|
自己编写的 `go func(){...}` 最终被编译器转换为对 `runtime.newproc` 的调用:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func newproc(fn *funcval) {
|
|||
|
|
gp := getg()
|
|||
|
|
pc := getcallerpc()
|
|||
|
|
|
|||
|
|
systemstack(func() {
|
|||
|
|
newg := newproc1(fn, gp, pc) // ① 切换到 g0 栈 → 构造 G 实例
|
|||
|
|
_p_ := getg().m.p.ptr()
|
|||
|
|
runqput(_p_, newg, true) // ② 放入就绪队列
|
|||
|
|
if mainStarted { wakep() } // ③ 唤醒空闲 P
|
|||
|
|
})
|
|||
|
|
// 切回原用户 G 继续执行
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**核心步骤**:
|
|||
|
|
|
|||
|
|
1. **切换栈**:通过 `systemstack` 从当前用户 G 栈切到 M 的 `g0` 栈——创建 G 是调度层面的工作,必须交由 `g0` 处理。
|
|||
|
|
2. **构造 G**:`newproc1` 分配并初始化新的 `g` 结构体(设置入口地址、程序计数器等)。
|
|||
|
|
3. **入队**:`runqput` 按优先级放置:`runnext` → LRQ → GRQ(满时通过 `runqputslow` 批量迁移)。
|
|||
|
|
4. **唤醒**:若有休眠中的 P,`wakep` 将其拉起来干活。
|
|||
|
|
|
|||
|
|
### 3.3 调度循环:从 g0 到 g
|
|||
|
|
|
|||
|
|
每个 M 的生命周期在两种角色间循环:**执行 `g0`(找任务)** ↔ **执行用户 G(做任务)**。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
A["M 执行 g0"] -->|"schedule()"| B["findRunnable()"]
|
|||
|
|
B -->|"找到 G"| C["execute(gp)"]
|
|||
|
|
C -->|"gogo()"| D["M 执行用户 G"]
|
|||
|
|
D -->|"G 让渡 / 阻塞 / 结束"| A
|
|||
|
|
style A fill:#fff9c4
|
|||
|
|
style D fill:#e8f5e9
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
关键桩函数:
|
|||
|
|
|
|||
|
|
| 函数 | 方向 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `mcall` / `systemstack` | G → g0 | 用户代码中切换至调度栈 |
|
|||
|
|
| `gogo` | g0 → G | 恢复用户 G 的上下文,交还 CPU |
|
|||
|
|
|
|||
|
|
### 3.4 findRunnable:寻找任务的优先级策略
|
|||
|
|
|
|||
|
|
这是调度环最核心的函数,按固定优先级依次查找:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
A["每 61 次检查 GRQ<br/>防饥饿"] -->|"命中"| Z["返回 G ✓"]
|
|||
|
|
A -->|"未命中"| B["LRQ: runqget<br/>无锁 CAS"]
|
|||
|
|
B -->|"有"| Z
|
|||
|
|
B -->|"空"| C["GRQ: globrunqget<br/>加锁"]
|
|||
|
|
C -->|"有"| Z
|
|||
|
|
C -->|"空"| D["netpoll<br/>非阻塞 epoll_wait"]
|
|||
|
|
D -->|"有"| Z
|
|||
|
|
D -->|"空"| E["work-stealing<br/>随机偷其他 P 的一半"]
|
|||
|
|
E -->|"成功"| Z
|
|||
|
|
E -->|"失败"| F["再次检查 GRQ"]
|
|||
|
|
F -->|"有"| Z
|
|||
|
|
F -->|"空"| G["释放 P → pidleput<br/>阻塞 netpoll 最后一次机会<br/>否则 stopm → 休眠"]
|
|||
|
|
style A fill:#e8f5e9
|
|||
|
|
style B fill:#c8e6c9
|
|||
|
|
style C fill:#fff3e0
|
|||
|
|
style D fill:#fff9c4
|
|||
|
|
style E fill:#ffebee
|
|||
|
|
style G fill:#eceff1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
源码路径:`runtime/proc.go`
|
|||
|
|
|
|||
|
|
### 3.5 本地与全局队列获取细节
|
|||
|
|
|
|||
|
|
#### 本地队列(无锁)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func runqget(_p_ *p) (gp *g, inheritTime bool) {
|
|||
|
|
// 先尝试 runnext VIP 位
|
|||
|
|
next := _p_.runnext
|
|||
|
|
if next != 0 && _p_.runnext.cas(next, 0) {
|
|||
|
|
return next.ptr(), true
|
|||
|
|
}
|
|||
|
|
// CAS 自旋取头部
|
|||
|
|
for {
|
|||
|
|
h := atomic.LoadAcq(&_p_.runqhead)
|
|||
|
|
t := _p_.runqtail
|
|||
|
|
if t == h { return nil, false }
|
|||
|
|
gp := _p_.runq[h%256].ptr()
|
|||
|
|
if atomic.CasRel(&_p_.runqhead, h, h+1) {
|
|||
|
|
return gp, false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`runnext` 存放刚创建的或高优先级的 G,跳过队列开销直接执行。
|
|||
|
|
|
|||
|
|
#### 全局队列(加锁)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func globrunqget(_p_ *p, max int32) *g {
|
|||
|
|
assertLockHeld(&sched.lock)
|
|||
|
|
if sched.runqsize == 0 { return nil }
|
|||
|
|
gp := sched.runq.pop()
|
|||
|
|
return gp
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!question] ❓ 为什么需要锁?
|
|||
|
|
> LRQ 是 per-P 私有数据,并发操作可通过 CAS 保证原子性;GRQ 被所有 P 共享,必须用互斥锁。
|
|||
|
|
|
|||
|
|
### 3.6 网络 IO 事件处理
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Net as netpoll
|
|||
|
|
participant EP as epoll_wait
|
|||
|
|
participant Q as gList
|
|||
|
|
loop 遍历就绪事件
|
|||
|
|
Net->>EP: epollwait(fd, events, 128, timeout)
|
|||
|
|
EP-->>Net: n 个就绪事件
|
|||
|
|
Net->>Q: netpollready(event)
|
|||
|
|
end
|
|||
|
|
Note over Q: 返回待运行的 G 列表
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
位于 `runtime/netpoll_epoll.go`,每次最多批量处理 128 个事件。
|
|||
|
|
|
|||
|
|
### 3.7 Work-Stealing 工作窃取
|
|||
|
|
|
|||
|
|
当本地和全局队列为空时,P 会从其他繁忙的 P 那里"偷"一半 G:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func stealWork(now int64) (...) {
|
|||
|
|
const stealTries = 4 // 最多试探 4 轮
|
|||
|
|
for i := 0; i < stealTries; i++ {
|
|||
|
|
for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
|
|||
|
|
p2 := allp[enum.position()]
|
|||
|
|
if pp == p2 { continue } // 不偷自己
|
|||
|
|
if idlepMask.read(enum.position()){ continue } // 目标也是 idle,跳过
|
|||
|
|
if gp := runqsteal(pp, p2, ...); gp != nil {
|
|||
|
|
return gp, ... // 偷到一半,成功!
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil, ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
设计要点:
|
|||
|
|
- **随机起始**:`fastrand()` 打乱探查顺序,避免热点竞争
|
|||
|
|
- **偷一半**:`runqsteal` 窃取目标 P LRQ 中的一半而非全部,减少反复争夺
|
|||
|
|
- **4 轮试探**:兼顾负载均衡与 CPU 利用率
|
|||
|
|
|
|||
|
|
### 3.8 P/M 回收机制
|
|||
|
|
|
|||
|
|
无事可做时,将闲置资源归还池中以节省 CPU:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 释放 P 并放回 pidle 队列
|
|||
|
|
releasep()
|
|||
|
|
_p_ = pidleput(_p_, now)
|
|||
|
|
|
|||
|
|
// 停止 M 并放入 midle 队列
|
|||
|
|
stopm() // → mPark() → pthread_cond_wait()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 💡 优雅缩容
|
|||
|
|
> sysmon 线程还会定期清理长时间处于 `_Psyscall` 状态的 P(见抢占篇),防止因系统调用导致 P 被白白占用。
|
|||
|
|
|
|||
|
|
### 4.1 让渡总览
|
|||
|
|
|
|||
|
|
G 拿到 CPU 后需要适时归还执行权,让给其他 G 使用。存在三种让渡方式:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
A["正在执行的 G"] --> B{"如何让渡?"}
|
|||
|
|
B -->|"正常结束"| C["goexit1 → goexit0<br/>状态→Gdead,回收复用"]
|
|||
|
|
B -->|"主动让出"| D["Gosched → gosched_m<br/>状态→Grunnable,入 GRQ"]
|
|||
|
|
B -->|"等待外部条件"| E["gopark → park_m<br/>状态→Gwaiting,上层保管"]
|
|||
|
|
C & D & E --> F["g0 调用 schedule() 寻找下一个 G"]
|
|||
|
|
style C fill:#e8f5e9
|
|||
|
|
style D fill:#fff9c4
|
|||
|
|
style E fill:#e3f2fd
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 功成身退:执行结束
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func goexit1() { mcall(goexit0) } // G → g0
|
|||
|
|
|
|||
|
|
func goexit0(gp *g) { // g0 执行
|
|||
|
|
casgstatus(gp, _Grunning, _Gdead) // 标记死亡
|
|||
|
|
dropg() // 解除 M 绑定
|
|||
|
|
gfput(_p_, gp) // 回收到 gfree 链表
|
|||
|
|
schedule() // 下一轮调度
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
被回收的 G 缓存在 P 的 `gfree` 链表中,下次 `newproc` 可直接复用,无需重新 malloc。
|
|||
|
|
|
|||
|
|
### 4.3 主动让出:Gosched
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func Gosched() { mcall(gosched_m) } // G → g0
|
|||
|
|
|
|||
|
|
func goschedImpl(gp *g) { // g0 执行
|
|||
|
|
casgstatus(gp, _Grunning, _Grunnable)
|
|||
|
|
dropg()
|
|||
|
|
lock(&sched.lock); globrunqput(gp); unlock(&sched.lock) // 入 GRQ
|
|||
|
|
schedule()
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] 📝 与 goexit 的区别
|
|||
|
|
> Gosched 将 G 放回 **GRQ**(等待再次调度),goexit 将 G 放回 **gfree**(彻底回收)。
|
|||
|
|
|
|||
|
|
### 4.4 情非得已:阻塞让渡(gopark / goready)
|
|||
|
|
|
|||
|
|
最常见的让渡方式——等待 channel 数据、Mutex、Timer 等外部条件触发。
|
|||
|
|
|
|||
|
|
**阻塞流程**(`gopark`):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func gopark(...) { mcall(park_m) } // G → g0
|
|||
|
|
|
|||
|
|
func park_m(gp *g) { // g0 执行
|
|||
|
|
casgstatus(gp, _Grunning, __Gwaiting)
|
|||
|
|
dropg()
|
|||
|
|
schedule() // g0 去找别的 G
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
关键点:`_Gwaiting` 状态的 G **不入任何就绪队列**,由上层调用者(channel / mutex 等)自行保管。
|
|||
|
|
|
|||
|
|
**唤醒流程**(`goready` → `ready`):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func goready(gp *g, traceskip int) {
|
|||
|
|
systemstack(func() { ready(gp, traceskip, true) })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ready(gp *g, traceskip int, next bool) { // g0 执行
|
|||
|
|
casgstatus(gp, _Gwaiting, _Grunnable)
|
|||
|
|
runqput(_g_.m.p.ptr(), gp, next) // 入 LRQ 或 GRQ
|
|||
|
|
wakep() // 唤醒空闲 P
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这一 `park` 一 `ready` 构成了 Go 并发原语的基础——G 级别阻塞,不拖垮 OS 线程。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-overview]] — GMP 概览
|
|||
|
|
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-datastructures]] — G/M/P/Schedt 数据结构
|
|||
|
|
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-preemption]] — 抢占式调度机制
|
|||
|
|
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-summary]] — 知识卡片
|