Go调度器原理浅析

The Go scheduler - Morsing's blog的翻译与整理

绪论(Introduction)

Go 1.1版本最重大的改进之一是由Dmitry Vyukov贡献的全新调度器。新调度器显著提升了并行Go程序的动态性能。虽然我已无法对这项成果做出更多贡献,但希望为它的传播尽一份力。

本文的大部分内容已包含在原始设计文档中,但该文档技术性较强。新调度器的核心思想可通过本文结合示意图更直观地理解。


为什么Go运行时需要一个调度器?

在深入新调度器之前,我们需要理解其必要性:既然操作系统已能调度线程,为何还要在用户空间实现调度器?
POSIX线程API本质上是对Unix进程模型的扩展,线程继承了进程的许多控制特性,例如信号掩码、CPU亲和性(绑定线程到特定CPU)、cgroups支持等。但这些特性对Go的goroutine模型来说是不必要的开销。当程序拥有10万个线程时,此类开销将显著增加。

此外,操作系统难以针对Go模型做出最优决策。例如,垃圾回收(GC )要求所有线程暂停且内存处于一致状态。若线程分散在随机执行点,GC需等待所有线程到达一致状态。而Go调度器能确保调度仅在内存一致点发生,从而大幅减少GC等待时间。(ps:我自己的理解就是:调度器会在内存状态一致时进行上下文切换,也就是说在上下文切换的时内存是一致的。如果在此时触发了垃圾回收,调度器会暂停切换操作,等待垃圾回收完成后再继续调度。)


核心角色:M、P、G

当前主流的线程模型包括:

  1. N:1模型:多个用户线程运行在一个OS线程上。上下文切换快,但无法利用多核。
  2. 1:1模型:用户线程与OS线程一一对应。可充分利用多核,但上下文切换成本高。
  3. M:N模型:Go采用的折中方案,任意数量的goroutine(用户线程)映射到任意数量的OS线程,兼顾切换速度与多核利用率,但实现复杂。

为实现M:N调度,Go定义了三个核心实体:

  • M(Machine) :OS线程,由操作系统管理,类似标准POSIX线程。
  • G(Goroutine) :包含栈、指令指针等信息的轻量级协程。
  • P(Processor) :调度上下文,可视为单线程调度器的本地化版本。P的数量由GOMAXPROCS决定,通常等于CPU核心数。


示意图:两个OS线程(M)各绑定一个上下文(P),每个P运行一个goroutine(G)。灰色G处于就绪状态,存储在运行队列(runqueue)中

每个P维护一个本地运行队列 。当创建新goroutine(通过go关键字)时,G会被加入当前P的队列尾部。P在执行完当前G后,从队列头部取出下一个G执行。

早期版本使用全局队列并通过互斥锁保护,但在多核机器上锁竞争严重。新调度器通过本地队列减少竞争。


系统调用与上下文切换

当G执行系统调用(如文件I/O)时,当前线程(M0)会阻塞。此时,调度器将P从M0剥离,并分配给其他线程(M1)继续执行其他G。

示意图:M0阻塞时,P转移至M1,M1执行其他G **

系统调用返回后,M0需尝试获取P来恢复执行原G。若无法获取,G会被放入全局队列,M0进入线程池或休眠。全局队列中的G会被其他P周期性检查以避免饿死。


工作窃取(Work Stealing)

当某P的本地队列为空时,它会按以下顺序获取G:

  1. 全局队列获取一批G。
  2. 若全局队列为空,从其他P的本地队列窃取半数G

此机制确保负载均衡,使所有线程尽可能满负荷工作。


调度器的其他细节

  • 网络轮询器(Netpoller) :处理I/O多路复用(如epoll),将阻塞的G挂起,待I/O就绪后重新加入队列。
  • 系统监控器(Sysmon) :后台线程,负责释放闲置内存、强制GC、处理长时间阻塞的P、抢占运行超时的G(10ms以上)。
  • 协作式抢占:Go 1.2~1.13通过函数调用插入抢占点;Go 1.14+基于信号实现真正抢占。

总结

Go调度器通过M:N模型在用户态实现高效协程调度,避免了内核态切换的开销。其核心创新在于引入P作为逻辑处理器,结合本地队列、全局队列和工作窃取机制,实现了高并发与低延迟的平衡。


参考

The Go scheduler - Morsing's blog
Go调度器深入解析-CSDN博客

相关推荐
可爱的霸王龙5 小时前
SpringBoot整合JWT
java·后端·jwt
六个点5 小时前
面试中常见的手写题汇总
前端·javascript·面试
爱的叹息5 小时前
Spring容器从启动到关闭的注解使用顺序及说明
java·后端·spring
蜡笔小祎在线学习5 小时前
小林coding-12道Spring面试题
java·后端·spring
知否技术5 小时前
Node登陆认证实战!10分钟手把手教会你!
后端·node.js
movee6 小时前
十分钟从零开始开发一个自己的MCP server(二)
后端·llm·mcp
movee6 小时前
十分钟从零开始开发一个自己的MCP server(一)
后端·llm·mcp
空气力学先驱6 小时前
自顶向下学习K8S--部署Agones
docker·云原生·容器·kubernetes·go
拉不动的猪6 小时前
react常规面试题
前端·javascript·面试
Adellle6 小时前
Java进阶
java·后端·面试