Go 运行时调度:GMP、阻塞与执行追踪
前置:goroutine 与 channel、取消与退出。本章目标是解释程序为什么等待、为什么加 goroutine 不一定更快,并能用追踪检查假设。教学版本 Go 1.27.2;示例不依赖外部服务,Windows 和 Linux 的实际执行属于历史验证记录,本轮未执行程序或追踪工具;macOS 仅保留平台说明,本项目不执行平台运行验证。
GMP:goroutine、线程与并行度
Go 的运行时把许多 goroutine 调度到操作系统线程上。G 是 goroutine 的运行状态(栈、执行位置等);M 是操作系统线程;P 是执行用户 Go 代码所需的一组调度与分配资源。M 执行用户 Go 代码时需要 P。GOMAXPROCS 控制同时执行用户 Go 代码的并行度,不是 goroutine 数,也不是线程数上限。
可以有许多就绪 G 等待少量 P;M 阻塞在某些系统调用时,运行时可将 P 交给别的 M。网络等待通常由运行时网络轮询配合唤醒,不能把每次网络等待都理解为独占一个线程。cgo、LockOSThread 和系统调用还有额外约束,本课程不使用它们。
调度器使用本地/全局运行队列和工作窃取等策略分配工作;抢占使长时间运行的 Go 工作有机会让出执行资源。这些是实现机制,不保证任意两个 goroutine 的先后、公平时限、具体队列长度或固定线程数。不要用调度器“会很快轮到它”替代同步协议。容器 CPU 限额和运行时版本会影响默认并行度;本例显式设置并在结束时恢复,生产程序应先检查实际配置和负载。
goroutine 阻塞、唤醒与有界工作
下面是完整 main.go。新目录执行 go mod init example.com/scheduler,go.mod 使用 go 1.27.0,然后 go run .。只有一个 P,三个 G 仍可分别到达等待点;等待 channel 的 G 不执行忙循环。主 G 确认全部到达后释放屏障,再等待结果。
package main
import (
"fmt"
"runtime"
)
func main() {
old := runtime.GOMAXPROCS(1)
defer runtime.GOMAXPROCS(old)
ready := make(chan struct{}, 3)
release := make(chan struct{})
done := make(chan int, 3)
for n := 1; n <= 3; n++ {
go func(n int) {
ready <- struct{}{}
<-release
done <- n * n
}(n)
}
for range 3 { <-ready }
fmt.Println("ready", 3)
close(release)
sum := 0
for range 3 { sum += <-done }
fmt.Println("sum", sum)
}预期输出:
ready 3
sum 14这个结果证明协议让三个工作完成,没有证明三个工作并行执行,也没有证明执行顺序是 1、2、3。把并行度改成 2,输出仍相同,只有运行机会分配可能改变。单纯给每项任务开 G,会增加栈、调度和 GC 根的成本;CPU 密集负载需考虑可用 CPU,I/O 密集负载还受连接池、下游容量和超时约束,见有界执行器。
常见误判:time.Sleep 只是当前 G 暂停,不保证另一项工作已完成;runtime.Gosched 让出执行机会,也不建立数据发布顺序。忙等一个普通 bool 既可能耗尽 CPU,又可能有数据竞争,正确通知需使用 channel/锁/原子协议。
execution trace:先区分等待再考虑优化
完整可追踪程序与测试在仓库 examples/go/system-lab,源码见综合实践。cmd/lab/main.go 用 trace.NewTask 标记批次、trace.StartRegion 标记每项计算。进入模块目录执行,PowerShell/Bash 相同:
go run ./cmd/lab
go tool trace -d=parsed trace.out
go tool trace -pprof=sched trace.out > sched.pprof
go tool pprof -top sched.pprof
go tool trace -http=127.0.0.1:6062 trace.out第一条预期输出 [1 4 9 16],在当前目录覆盖生成 trace.out。-d=parsed 输出事件文本,能找到任务/区域与调度状态变化;调度 profile 是就绪后等待运行的延迟分布,不是函数 CPU 时间。最后一条启动仅本机可访问的查看器,用完 Ctrl+C 停止;默认工具可能尝试打开浏览器,核心验收使用前面可解析的文件。二进制重定向使用 Bash 或 PowerShell 7.4+;旧 PowerShell 请用 Bash 生成 profile,避免文本转码。
读图顺序:先看总时段和任务区间,再找 goroutine 在等待同步、系统调用还是就绪队列;接着检查是谁唤醒它,最后才估计并行度是否合适。本例计算很短,不能据此得出吞吐率结论;调度 profile 可能只有少量事件。要判断业务瓶颈,应录制有代表性且足够长的负载,并与 CPU/profile 和业务指标互证。追踪有额外开销,文件大小和事件数随运行变化。
练习与自检
- 先解释若主 G 在等待 ready 前就等待 done,而工作 G 改为无缓冲 ready,会发生什么。
- 在原例中分别使用并行度 1、2,记录相同的正确性输出;说明为什么不能从输出推断并行速度。
- 运行完整追踪项目,找到
bounded-batch与square。提出“CPU 不足”和“工作在等锁”两种假设,各自需要什么证据?
参考解法
无缓冲 ready 的发送者等接收,主 G 却等它们之后才会发送的 done,形成循环等待;增加 Sleep 不会解除依赖环。原例的 ready 接收建立阶段屏障,done 接收建立完成屏障,与执行交错无关。CPU 不足需看到持续的可运行工作和运行资源忙碌;等待锁需查阻塞位置、锁持有者和临界区,CPU profile 本身不记录大部分等待时间。不要给这个微型负载编造固定加速比。
依据:运行时 G/M/P 与栈、runtime 配置、execution trace、诊断工具。实现细节不视为语言保证,核对日期 2026-10-17。
继续阅读
- 开发实践
- 开发实践
- 开发实践