goroutine切换仅保存PC、SP和偶尔BP,不进内核、不换地址空间,纯用户态完成,耗时约100ns;而线程切换需保存全部寄存器并陷入内核,耗时1–5μs。

goroutine 切换不是“没有上下文切换”,而是切换成本极低的用户态上下文切换——它不进内核、不换地址空间、只存 PC 和 SP,所以快。
goroutine 切换到底保存哪些寄存器
Go 的 goroutine 切换只保存最核心的两个状态:PC(程序计数器)和 SP(栈指针),偶尔加 BP(帧基址)。它不保存全部 CPU 寄存器,更不切换页表或内核栈。这是因为 goroutine 始终运行在同一个 OS 线程(m)上,共享地址空间与内核态上下文。
对比线程切换:后者必须保存所有通用寄存器 + 段寄存器 + 页表基址(CR3)+ 内核栈指针,且需从用户态陷入内核态再返回,耗时 1–5μs;而 goroutine 切换纯在用户态完成,平均约 100ns。
常见误判点:
立即学习“go语言免费学习笔记(深入)”;
- 看到
runtime.gosched或chan send就以为“调度器主动干预”——其实只是当前 goroutine 主动让出,调度器只是顺势选下一个就绪的 - 认为“栈扩容触发切换”是开销来源——实际扩容本身不切换,但扩容前的函数调用检查会插入调度点,属于被动让出
- 用
pprof看到runtime.schedule占比高,就归因为“切换慢”——其实是 goroutine 数量太多、排队太长,调度器忙于找活干,不是切换动作变慢
哪些操作会触发 goroutine 切换
Go 调度器是非抢占式的,切换只发生在明确的协作点。以下操作会触发:
-
channel发送/接收(缓冲区满/空时阻塞) - 网络 I/O(如
net.Conn.Read)、文件 I/O(os.File.Read)等系统调用封装点 - 显式调用
runtime.Gosched()或time.Sleep() - 垃圾回收器 STW 阶段强制暂停(极少,属特殊场景)
- 栈增长检查(函数调用前 runtime 插入的栈空间检查逻辑)
注意:纯 CPU 循环不会触发切换。比如 for {} 或密集浮点运算,会一直霸占所在 P 的时间片,导致其他 goroutine 饥饿——这不是 bug,是设计使然。解决方式只能是插入调度点(如 runtime.Gosched())或拆成小块任务交由 channel 分发。
为什么大量 goroutine 会让 runtime.schedule 占 CPU
当活跃 goroutine 数量远超 runtime.NumCPU() * 100 时,P 的本地队列常为空,M 不得不频繁跨 P 窃取(findrunnable),引发 cache line 失效、锁竞争(allglock)、全局队列争抢。此时 runtime.schedule 不是在做“切换”,而是在疯狂扫描:查本地队列、查全局队列、查 netpoll、查定时器、查被偷走的 goroutine……
典型信号:
-
go tool pprof -http=:8080中runtime.schedule占比 >25%,而业务 handler 函数占比极低 -
/debug/pprof/goroutine?debug=2显示大量 goroutine 停在chan receive、semacquire、netpoll -
runtime.NumGoroutine()持续 >50k,但 QPS 并未线性提升,P99 延迟陡升
这不是 goroutine “轻”的失效,而是调度元数据膨胀 + 查找路径变长导致的线性退化。10 万个 goroutine ≈ 200MB 栈内存 + GC 扫描压力 + g 结构体本身开销(约 40B × 100k = 4MB)。
goroutine 切换与 GMP 模型的关系
goroutine 切换动作本身由 g0(每个 M 绑定的调度专用 goroutine)执行:当前 g → g0 → 下一个 g。整个过程不涉及 P 的切换,除非当前 P 的本地队列已空且找不到可运行的 goroutine,才会触发 work-stealing——这时才需要访问其他 P 的 runq,带来额外开销。
关键事实:
-
P是逻辑处理器,数量默认 =runtime.NumCPU(),它持有本地运行队列(runq,长度 256)和全局队列(global runq) -
M是 OS 线程,一个 M 最多绑定一个 P,但可临时解绑去执行系统调用(此时 P 可被其他 M 接管) -
g的状态迁移(_Grunnable→_Grunning→_Gwaiting)由 runtime 在这些点上原子更新,切换只发生在状态变更瞬间
真正影响性能的,从来不是单次切换动作,而是 goroutine 生命周期管理是否合理:是否该复用、是否该限流、是否该用 worker pool 控制并发度——这些决策落在应用层,而非 runtime。


















