不是,runtime.findrunnable() 是为已绑定 P 的 M 寻找下一个可运行 G 的入口,不负责 P 的获取;M 获取 P 的实际入口是 acquirep(),通过空闲 P 链表 CAS 获取或等待唤醒。

runtime.findrunnable() 是 M 获取空闲 P 的实际入口吗?
不是。M 本身不“查找 P”,而是“持有 P”;findrunnable() 的职责是为当前已绑定 P 的 M 寻找下一个可运行的 G,它不负责 P 的分配或切换。真正触发 M 获取(或重新获取)P 的动作,发生在 M 从阻塞状态恢复、或刚被创建、或原 P 被移交后进入调度循环时——此时调用的是 acquirep()。
acquirep() 如何从 allp 数组中拿到一个可用 P?
acquirep() 不做线性遍历,也不加锁轮询。它依赖两个关键机制:
- 每个 M 在初始化时会预绑定一个 P(通过
allp[id]直接索引),id 来自全局原子计数器pidle; - 当 M 需要“重新 acquire”(比如系统调用返回后),它会尝试从全局空闲 P 链表
pidle中 CAS 弹出一个 P;若链表为空,且gomaxprocs未满,则新建 P 并加入 allp;否则阻塞等待。
注意:acquirep() 成功的前提是该 P 的 status 为 _Pidle,且其 m 字段为 nil —— 这是 runtime 保证 P 不被重复获取的核心检查点。
为什么 M 不能直接从其他正在运行的 P 上“偷”一个 P?
因为 P 不可被“偷”,只能被“移交”或“释放”。GMP 模型中 P 是资源协调单元,不是任务单元。M 能做的只有:
立即学习“go语言免费学习笔记(深入)”;
- 从空闲池(
pidle)拿一个 P; - 等待某个正在运行的 P 主动调用
handoffp()把自己交出来(例如原 M 进入系统调用且 P 上还有待运行 G); - 在极端情况下,触发
stopm()后由startm()唤醒并间接关联新 P。
试图绕过这些路径强行抢占 P,会破坏调度器对 P 状态机(_Pidle → _Prunning → _Psyscall → _Pidle)的严格管控,导致 schedule() 死循环或 panic。
acquirep() 失败时 M 会卡住吗?
不会立即卡住,但会进入休眠等待。具体行为取决于上下文:
- 若 M 刚完成系统调用,且无空闲 P,它会调用
notesleep(&m.park),直到被其他 M 或 netpoller 唤醒; - 若 M 是新创建的(如因
newm()触发),而gomaxprocs已满且无空闲 P,它会被挂起在idlem()队列中,不再参与调度; - GC 或栈扫描期间,
acquirep()可能被抢占信号中断,需重试而非死等。
这个等待逻辑藏在 stopm() 和 startm() 的协作里,而不是裸露在用户可见的 API 层 —— 所以调试时看到 M 长时间休眠,往往不是 bug,而是 P 资源耗尽的正常反馈。
真正容易被忽略的是:P 的生命周期完全由 runtime 控制,acquirep() 成功后,M 必须在退出前调用 releasep(),否则该 P 会永久脱离调度循环,造成后续所有 M 都无法获取新 P —— 这种泄漏比 goroutine 泄漏更隐蔽,也更致命。


















