Go的P有5种状态:_Pidle、_Prunning、_Psyscall、_Pgcstop、_Pdead;其中_Pidle和_Prunning为活跃态,_Psyscall用于系统调用期间,_Pgcstop和_Pdead为GC或GOMAXPROCS调整时的管理态。

P 状态有哪些?直接看 runtime 源码定义
Go 的 P(Processor)只有 5 种有效状态,全部定义在 runtime/runtime2.go 中:_Pidle、_Prunning、_Psyscall、_Pgcstop、_Pdead。其中 _Pidle 和 _Prunning 是最常出现的两个活跃态;_Psyscall 只在 P 所绑定的 G 正在执行系统调用时短暂存在;_Pgcstop 和 _Pdead 属于管理态,不参与常规调度。
常见 P 状态转换路径与触发条件
实际运行中,P 大多在 _Pidle ↔ _Prunning ↔ _Psyscall 之间流转,关键转换由以下动作驱动:
- 从
_Pidle→_Prunning:M 成功绑定一个空闲 P(例如新 M 启动、或阻塞 M 恢复后抢到 P) - 从
_Prunning→_Psyscall:当前 P 绑定的 G 调用阻塞式系统调用(如read、accept),且该调用未被网络轮询器接管 - 从
_Psyscall→_Pidle:系统调用返回,但此时原 M 已被 runtime 判定为“卡住”,P 被主动释放(避免 P 长期闲置) - 从
_Prunning→_Pidle:P 被显式停用(如runtime.GOMAXPROCS(n)缩小 P 数量),或 GC 前被临时摘下
注意:_Pgcstop 不是自发进入的——它由 GC 的 STW 阶段统一设置,所有非 _Pdead 的 P 都会被强制置为此态;GC 完成后,它们不会自动恢复,而是先被设为 _Pidle,再等待 M 来绑定。
为什么看不到 _Pidle → _Pgcstop 或 _Pdead 的直接跳转?
因为这两种状态变更不是由单个 P 自主决定的,而是由全局调度器协调完成:
-
_Pgcstop总是批量设置:GC 触发时,runtime.stopTheWorldWithSema()会遍历runtime.allp,将所有_Prunning和_Pidle的 P 改为_Pgcstop -
_Pdead只在调整GOMAXPROCS且新值小于当前 P 总数时发生:多余 P 的status被设为_Pdead,其本地队列中的 G 全部被转移到全局队列,p.gfree中的空闲 G 也归还给全局池
换句话说,_Pgcstop 和 _Pdead 是“外部强干预态”,P 自身代码里几乎不包含向这两个状态跃迁的逻辑分支。
调试时如何观察 P 的实时状态?
Go 运行时未暴露直接读取 P 状态的公开 API,但可通过以下方式间接验证:
- 启用调度追踪:
GODEBUG=schedtrace=1000,输出中每 1s 会打印类似SCHED 0ms: gomaxprocs=8 idleprocs=2 threads=15 spinningthreads=1 idlethreads=5 runqueue=0 [0 0 0 0 0 0 0 0],其中idleprocs就是当前_Pidle的 P 数量 - 用
pprof查看 goroutine trace:go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine?debug=2,可看到每个 G 所属 P 的 ID,结合runtime.GOMAXPROCS(0)可反推哪些 P 处于活跃态 - 源码级断点:在
runtime.procresize()或runtime.stopTheWorldWithSema()下断,观察p.status字段变化
真正容易被忽略的是:_Psyscall 状态本身不阻塞其他 M —— 它只表示“这个 P 当前无法调度新 G”,但 runtime 会立刻让出 P 给别的 M 使用;所以即使大量 G 卡在 syscall,只要 P 数量足够,整体吞吐未必下降——这正是 GMP 区别于传统线程池的关键设计点。


















