go 的 goroutine 调度基于用户态 gmp 模型,采用协作与抢占混合机制,不依赖 os 线程轮转;其执行需调度点触发(如函数调用、系统调用返回、gosched 或抢占),纯 cpu 循环无调度点将导致 goroutine 卡住。

Go 的 goroutine 调度不是靠操作系统线程轮转,而是 GMP 模型在用户态做的协作+抢占混合调度 —— 你写的 go f() 不会立刻执行,也不保证马上被 CPU 执行,更不等于一个 OS 线程。
goroutine 为什么有时不立即运行?
因为 runtime.schedule() 只在特定时机触发:比如当前 goroutine 主动让出(runtime.Gosched())、系统调用返回、函数调用栈增长检查点、或被抢占(如超过 10ms 的连续运行)。它不依赖时间片中断,也没有“就绪队列优先级”这种概念。
常见错误现象:for {} 死循环里起的 goroutine 一直卡住;select {} 后没反应;主 goroutine 退出后子 goroutine 没机会跑。
- 确保有调度点:避免纯计算无函数调用的长循环,可插入
runtime.Gosched()或小 sleep - 主 goroutine 别直接退出:用
sync.WaitGroup或time.Sleep()等待,否则整个程序退出,所有 goroutine 被强制终止 - 阻塞系统调用(如文件读写、网络收发)会自动让出 P,但纯 CPU 计算不会 —— 这是新手最常忽略的调度盲区
GMP 中的 P 被谁绑定?什么时候解绑?
P(Processor)是调度关键资源,每个 P 维护本地可运行队列。它默认最多与 M(OS 线程)一对一绑定,但仅当 M 处于执行状态且未被阻塞时才持有 P;一旦 M 进入系统调用或睡眠,P 就会被剥离,交给其他空闲 M 抢占。
使用场景:高并发 I/O 服务中,大量 goroutine 阻塞在 read() 上,此时 P 会快速流转,M 数量可能远小于 goroutine 数量。
-
GOMAXPROCS控制 P 的数量(默认等于 CPU 核数),不是线程数,也不是 goroutine 并发上限 - 设置过低(如
GOMAXPROCS=1)会导致所有 goroutine 串行化调度,哪怕有多个 CPU 核也用不上 - M 在系统调用返回前会尝试“窃取”其他 P,失败则进入休眠;唤醒后需重新获取 P 才能继续执行 goroutine
为什么 runtime.LockOSThread() 后 goroutine 不再迁移?
调用 runtime.LockOSThread() 会让当前 goroutine 和其所在 M 绑定,同时该 M 会独占一个 P(不再释放),后续所有由该 goroutine 启动的 goroutine 都只能在这个 P 上运行 —— 这是为了满足某些 C 代码对线程局部存储(TLS)或信号处理的强要求。
容易踩的坑:LockOSThread() 是 goroutine 级别的,不是函数级;一旦锁定,必须配对调用 runtime.UnlockOSThread(),否则该 M/P 组合永久被占用,可能导致其他 goroutine 饿死。
- 不要在 goroutine 入口随便加
LockOSThread(),尤其不确定是否需要跨平台 TLS 或 syscall 上下文时 - 锁定后若启动新 goroutine(比如
go f()),它们仍受同一 P 限制,无法利用其他 P 的空闲能力 - CGO 调用中若隐式触发
LockOSThread()(如某些库初始化),需检查文档,避免意外绑定
真正难调的是那些“看起来该跑却没跑”的情况——往往不是调度器坏了,而是你写的代码没给它留出调度机会,或者误判了 P/M 的生命周期。GMP 不是黑箱,但它只响应明确的信号:函数调用、系统调用、显式让出、抢占点。没这些,它就静静等着。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











