go-runtimelisted
Install: claude install-skill omeyang/Thoth
# Go 运行时原理专家
回答运行时原理问题或提供调试依据:$ARGUMENTS
**基线 Go 1.25.9+。所有内部机制以当前版本为准;历史演进作对比说明。**
---
## 为什么学运行时
**不是为了背书**,是为了:
1. 看懂 `GODEBUG=gctrace=1` / `schedtrace=1000` / `allocfreetrace=1` 输出
2. 判断"该不该优化"的根本依据(`sync.RWMutex` vs `Mutex`、`sync.Map` vs 分片锁)
3. 诊断异常行为:STW 抖动、goroutine 泄漏、调度延迟、OOM
4. 评估新版本影响(如 1.24 Swiss Tables 对 map 性能分布的改变)
想做具体优化 → `go-performance` 技能;这里讲**为什么**。
---
## §1 GMP 调度器
### 1.1 三角色
| 角色 | 含义 | 典型数量 |
|---|---|---|
| **G** (Goroutine) | 用户协程,栈 + PC + 状态 | 可达百万 |
| **M** (Machine) | OS 线程 | 少量,由 `GOMAXPROCS` + syscall 阻塞动态决定 |
| **P** (Processor) | 调度上下文,持有本地 runqueue | `GOMAXPROCS` 个 |
**关系**:
- M 必须绑 P 才能运行 G
- P 数量通常 = CPU 核心数(`GOMAXPROCS`)
- Go 1.25 起:Linux 下自动识别 cgroup CPU quota
### 1.2 调度循环
```
M 有 P,开始调度:
1. 从 P 的本地 runqueue 取 G(快速路径)
2. 如果本地空:
a. 从全局 runqueue 取(每 61 次也主动检查,避免饥饿)
b. 从其他 P 偷一半(work-stealing)
c. 检查 netpoll
3. 执行 G,直到阻塞或时间片到
4. 归还 P 给 M 池或主动让出
```
### 1.3 抢占
- **协作式**(Go 1.13 前):函数调用插入检查点,死循环无调用无法抢占
- **信号抢占**(Go 1.14+):`SIGURG` 强制中断,即使死循环也能抢
- **时间片**:10 ms,到期主动让出(异步抢占实现)
- `GODEBUG=asyncpreemptoff=1` 关闭信号抢占(调试用;生产勿开)
### 1.4 系统调用阻塞
```
G 执行 syscall(阻塞性):
1. M 脱离 P(sysmon 监测到阻塞 > 20μs)
2. P 被其他 M 接管继续调度其他 G
3. syscall 返回后 M 尝试拿 P:
a. 有空闲 P → 继续
b. 无 → G 放入全局 runqueue,M 睡眠
```
### 1.5 netpoll
非阻塞 I/O(net/http、database/sql 底层)用 netpoll:
- Linux: epoll
- macOS/BSD: kqueue
- Windows: IOCP
**关键**:G 在 netpoll 等待时**不占 P、不占 M**。这是 Go 能起百万 goroutine 的根本。
### 1.6 GODEBUG schedtrace
```ba