上篇分别记录了分配、回收和服务等待完成的时间。这些数字还不能说明等待发生在 worker 的哪一处:正在消耗 CPU、卡在 channel 接收,还是等待网络可读?本篇继续同一个任务服务,把现有 worker 的分析函数切换到三种受控入口,保留 完整源码与证据包、SHA-256 和 独立复跑步骤。

RUNNING 不等于正在占用 CPU

TestWorkerWaitKinds 仍使用真实的 POST /tasks handler、当前任务存储和一个 worker。先发送空文本并确认 400、没有占用任务 ID;再提交 Go one,收到 202 才等待 worker 进入分析函数。此时任务记录均为 RUNNING,但这只是业务状态,无法告诉调度器此时的 G 在做什么。

三种分析函数只改变阻塞点。CPU 场景在等待释放的循环里给原子计数器加一;channel 场景在 release channel 上接收;network 场景连接本机 TCP,执行实际 net.Conn.Read,对端在测试放行前不写字节。对每种场景,主测试在 worker 报告到达检查点之后读取任务记录,并通过 pprof.Lookup("goroutine").WriteTo(..., 2) 保存所有 goroutine 的原始栈。截取包含该分析函数的栈,冻结工具链实际报告 CPU 场景为 [runnable] 或 [running]、channel 为 [chan receive]、网络等待为 [IO wait]。这些标签是快照,不是该 G 整个任务周期的 CPU 使用率;CPU 计数大于零也不是耗时量化。

网络 case 的 started 信号在调用 Read 前发出,单靠这个信号不能断言它已经进入 IO 等待;所以测试轮询 profile,直到同一分析器栈确实带有 [IO wait] 才记录,超过 2 秒就让测试失败。TCP Read 的 5 秒期限只作防挂保护,不用睡眠伪造执行顺序。随后 channel/CPU 关闭各自 release 闸门,网络由对端写一个字节;调用服务 Close 等待自己的 worker,三种任务才分别读到 SUCCEEDED words=2。释放闸门与请求取消不是一回事,先前那条 HTTP 202 也不是业务完成凭据。

这条栈在固定源码里对应什么

冻结版本是 Go 1.27.1,源码 tag go1.27.1 的镜像查询完整 SHA 为 862c888e612ac346c7c4d99c9392bdfd265f33b0;本轮又对实际安装的 src/runtime/proc.go、chan.go、netpoll.go 等文件逐个存了 SHA-256 与源码片段。该 tag 来自此前镜像查询,官方 Gitiles 交叉核验尚未完成,不能将其冒称官方复核。

当前工具链的 runtime/chan.go:chanrecv 在无数据可收时选择 waitReasonChanReceive 并调用 gopark;runtime/netpoll.go:netpollblock 的 IO 等待分支选择 waitReasonIOWait 进入 gopark。runtime/proc.go:findRunnable 会从一次非阻塞 netpoll(0) 返回的可运行列表中唤醒 G,代码中能看到 _Gwaiting 到 _Grunnable 的状态转移。TCP 调用栈还实际经过 net.(*conn).Read 与 internal/poll.(*FD).Read;源码路径和这次 [IO wait] 的快照相互支持,不能把 Read 等价为“每次都会开新 OS 线程”。

另一面,不能把这个网络轮询例子写成“M 永远不会阻塞”:其他系统调用、cgo、锁、调度状态和平台实现各有不同路径;本章既没追所有 M 的状态,也没量化调度器运行时间。runtime/proc.go:schedule 和 findRunnable 处理的不仅是网络可读事件,channel 释放后变成可运行,也不保证立即取得 CPU。固定源码给出当前实现的一条调用链;语言级别不规定这些内部函数名字。

练习:① 查询返回 RUNNING 就能说 worker 正忙于 CPU 吗?不能,本次 channel 与 TCP 两个场景在 RUNNING 状态下的分析器栈分别处于接收和 IO 等待。② TCP 连接已建立是否等于 Read 已经开始等待?不能,必须通过实际栈快照或同步事件确认;started 在 Read 之前关闭。③ Close 前写入对端字节,能否把 HTTP 202 当最终结果?不能,必须等 worker 退出并读取 SUCCEEDED/words=2;错误路径还要核对失败终态,不允许靠单条状态标签推断完成。

writing-plans/go-language/evidence/34/ 保存十轮相同负载的原始 profile 栈、五个固定源码文件哈希、实际文件/符号与源码片段、真实 PostgreSQL 16.15 的累计回归、-race 执行路径以及旧 CLI 正反日志。SQL 没有进入这三个调度场景;无 race 报告不证明其他交错安全;数据库提交和外部副作用仍分别判定。远程网络、其他平台、进程级调度跟踪、含 34 的全站构建和发布仍 NOT_RUN。下一篇才采样 CPU 与 heap profile 并尝试用相同负载确认单项优化。

参考资料