Go协程由runtime自动调度,你只需调用go f();它在安全点(如I/O阻塞、函数调用、内存分配等)主动让出,而非时间片强制中断;避免for{}死循环导致P被独占。

Go 的协程(goroutine)调度不是你手动“做”的事,而是 runtime 自动完成的——你只需写 go f(),剩下的交给 GMP 模型。真正要你操心的,是别写出让调度器“卡住”或“饿死其他协程”的代码。
goroutine 什么时候会被调度器切走?
它不会靠时间片轮转强制中断,而是在安全点(safepoint)主动让出。这些点不是你控制的,但你能触发:
-
time.Sleep()、chan收发(阻塞时)、net.Conn.Read/Write等标准 I/O:调度器立刻接管,M 脱离 P,其他 G 继续跑 - 函数调用(哪怕只是空函数):Go 1.14+ 会在调用指令前插入抢占检查,防止纯循环霸占 P
- 内存分配(如
make([]int, 1000))、GC 辅助工作、栈扩容:也会触发调度 -
runtime.Gosched()是显式让出,但极少需要——它只把当前 G 推回本地队列尾部,不解决根本阻塞问题
⚠️ 注意:for {} 或 for i := 0; i 这类无函数调用、无 I/O、无内存分配的纯计算循环,在 Go 1.14 前真会卡死整个 P;现在虽有异步抢占(SIGURG),但默认 10ms 才触发,仍可能造成可观延迟。
为什么 runtime.GOMAXPROCS(1) 后 goroutine 全串行?
因为 P 的数量被锁死了。每个 goroutine 必须绑定到一个 P 才能运行,而 P 是调度的基本单位。设成 1,就只剩 1 个本地运行队列,所有新 goroutine 都得排队等这个 P 空出来。
立即学习“go语言免费学习笔记(深入)”;
- 这不是“并发失效”,而是“并行度为 1”——I/O 仍可非阻塞(netpoller 照常工作),但 CPU 密集任务无法重叠执行
- HTTP server 默认每请求起 goroutine,若
GOMAXPROCS=1,高并发下请求会积压在单个队列里,RTT 显著升高 - 调试时临时设为 1 可复现竞态,但上线千万别这么配;现代服务通常保持默认(等于 CPU 核数)或略调高
goroutine 创建太快,为啥有些“迟迟不执行”?
新 goroutine 默认进全局队列,而调度器优先从 P 的本地队列取任务。只有当本地队列为空,才会去全局队列“搬活”,且每次最多搬 1/4(硬编码策略),还可能触发 work-stealing 去偷别的 P 的本地队列——中间存在最多 61 次调度间隔的延迟。
- 高频创建场景(如
for i := range reqs { go handle(i) })容易挤爆全局队列,新启动的 P 初期可能空转几轮才分到任务 - 变量捕获错误(如共用
i)不是调度问题,但现象类似“某些 goroutine 行为错乱”,常被误判 - 想降低延迟?用 worker pool 复用 goroutine,或批量处理(如
go func(batch []req) { ... }(chunk)),减少新建频次
最易被忽略的一点:CGO 调用中若阻塞过久,又没调 runtime.LockOSThread(),M 可能被回收,唤醒后找不到原 P,导致 goroutine “失踪”几毫秒甚至更久——这不是调度器慢,是线程生命周期管理出了岔子。


















