gomaxprocs=1时goroutine仍可交替运行,因p仅提供调度环境,真正切换依赖goroutine主动让出(如sleep、gosched、channel、系统调用),触发p交还调度器实现单p上的并发调度,非并行。

P 是 Go 调度器中真正决定“并发能力上限”的逻辑单元,不是 CPU 核心的镜像,而是一个可配置、有硬限制、带本地队列的调度上下文容器。
为什么 GOMAXPROCS=1 时 goroutine 还能交替运行
因为 P 不是执行体,它只提供调度环境;真正切换靠的是 goroutine 主动让出控制权。只要发生 time.Sleep、runtime.Gosched()、channel 操作或系统调用,当前 M 就会把 P 交还给调度器,从而 pick 另一个就绪的 G——这属于并发(concurrency),不是并行(parallelism)。
- 计算密集型循环(如
for i := 0; i )若无函数调用、无栈增长、无阻塞点,Go 1.14+ 的 <code>sysmon线程会定期发SIGURG抢占,但延迟可能达数百毫秒,导致其他 G “饿死” -
runtime.LockOSThread()不影响 P 数量,只绑定 M 到 OS 线程;若该 M 已绑 P,调度逻辑照常 - P 本身不会被“抢占”,所谓“P 被抢占”实质是 M 解绑当前 P,转而尝试获取空闲 P 或休眠
P 的数量如何影响实际调度行为
P 数量直接决定最多有多少个 G 能同时处于“可立即执行”状态(即绑定在某个 P 的本地队列中)。它不是越多越好,也不是越少越省资源。
- 默认值是
runtime.NumCPU(),适用于大多数服务端程序 - CPU 密集型任务:P 接近物理核心数更稳;超线程数(如 16 逻辑核对应 8 物理核)不建议直接照搬
- I/O 密集型任务:可适当提高(如 ×1.5),但超过 256 无效(
GOMAXPROCS硬上限) - P 过多 → work-stealing 频繁、本地队列利用率下降、cache line false sharing 加剧
- P 过少 → I/O + 计算混合场景下容易成为瓶颈,尤其当多个 M 因系统调用阻塞后无法及时获取空闲 P
P 的本地队列与全局队列怎么协作
每个 P 维护一个长度仅 256 的本地运行队列(runq),这是调度效率的关键设计,也是最容易被忽略的隐性瓶颈。
- 新创建的 G 默认入当前 P 的本地队列;满则溢出至全局队列(
global runq) - M 优先从绑定 P 的本地队列取 G;本地队列空了,才去全局队列偷;全局队列也空了,再尝试从其他 P 的本地队列“steal”
- 全局队列是全局锁保护的,争抢成本高;频繁溢出到全局队列意味着本地队列压不住任务节奏
- 系统调用返回时,若原 P 已被占用,M 会把 G 放进全局队列,自己休眠——此时 G 的延迟取决于全局队列长度和唤醒时机
真正复杂的地方在于:P 的数量、本地队列长度、work-stealing 触发条件、sysmon 抢占时机,四者交织影响响应毛刺和吞吐稳定性。改 GOMAXPROCS 前,先看 runtime.ReadMemStats 里 NumGC 和 PauseNs 是否异常,再结合 pprof 的 goroutine trace 判断是不是本地队列长期积压或频繁偷任务。











