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

286 lines
9.0 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, 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]] — 知识卡片