基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Go 如何实现抢占式调度?

知识点图片

Go 语言的抢占式调度(Preemptive Scheduling)经历了一个演进过程。目前的 Go 版本(1.14+)采用的是基于信号的异步抢占式调度

在此之前,Go 采用的是协作式调度。为了讲清楚“如何实现”,我们需要对比这两个阶段,重点解释 Go 1.14 引入的机制。


1. 早期方案:协作式抢占(Cooperative Preemption)

(Go 1.14 之前)

在 Go 1.14 之前,调度器依赖于用户代码的“配合”。

  • 实现原理:
    Go 编译器会在每个函数调用的开头(Function Prologue)插入一段检查栈空间的代码(morestack)。
    当运行时(Runtime)想要抢占某个 Goroutine(G)时,它不会直接强制停止 G,而是将该 G 的栈扩容标志(stackguard0)设置为一个特定的“抢占标志”。
    当 G 执行到下一次函数调用时,会检查到这个标志,意识到“我需要交出 CPU 了”,于是主动调用调度器让出 P。

  • 致命缺陷:
    如果一个 Goroutine 运行在一个死循环中(例如 for { i++ }),且循环内部没有任何函数调用,那么它永远不会执行栈检查。这会导致该 P 被长期占用,GC(垃圾回收)无法启动(STW 等待),甚至导致整个程序卡死。


2. 当前方案:基于信号的异步抢占(Asynchronous Preemption)

(Go 1.14 及以后)

为了解决上述“死循环无法被抢占”的问题,Go 引入了基于系统信号的异步抢占。其核心流程涉及 Sysmon 线程操作系统信号信号处理函数

核心组件

  1. Sysmon(System Monitor):
    这是一个独立于 GMP 模型之外的系统监控线程,它不需要绑定 P 就可以运行。它会周期性地(约 10ms)苏醒,检查系统的运行状态。

  2. SIGURG 信号:
    Go 选择了 SIGURG 信号作为抢占信号。选择它的原因是该信号主要用于带外数据传输,在现代系统中很少被用户程序主动使用,冲突概率低,且对 I/O 操作干扰小。

详细实现步骤

Step 1: 监控与标记 (Sysmon)
sysmon 线程在循环检查时,会遍历所有的 P(Processor)。如果发现某个 P 上的 G 运行时间过长(超过 10ms),就会判定该 G 需要被抢占。

Step 2: 发送信号 (preemptM)
sysmon 会向运行该 G 的 M(系统线程)发送一个 SIGURG 信号。

Step 3: 操作系统中断
操作系统收到信号后,会中断 M 当前正在执行的代码,挂起当前的执行流,转而去执行 Go 运行时注册的信号处理函数sighandler)。

Step 4: 信号处理与上下文注入
在信号处理函数中,Go 运行时会检查当前是否适合抢占(例如是否处于原子操作、是否持有锁等不安全点)。
如果确认安全,运行时会修改被中断线程的程序计数器(PC)寄存器上下文。它将原本要执行的下一条指令地址,修改为指向运行时的一个特殊函数 —— asyncPreempt

Step 5: 恢复执行与让出
信号处理函数结束,操作系统恢复 M 的执行。
由于上下文被修改,M 醒来后不会继续执行原来的死循环代码,而是立即跳转执行 asyncPreempt 函数。

Step 6: 执行调度
asyncPreempt 会保存当前的全局状态(寄存器等),然后调用 gopreempt_m,将当前的 G 放入全局运行队列(Global Run Queue),并重新进入调度循环(Schedule),寻找下一个可运行的 G。


3. 总结:流程图解

简单来说,Go 实现抢占式调度的逻辑链条如下:

  1. Sysmon 监控:发现 G 运行太久(>10ms)。
  2. 发送信号:Sysmon 向目标线程 M 发送 SIGURG
  3. OS 中断:M 暂停手头工作,进入信号处理。
  4. 插桩:信号处理器修改 M 的下一步指令为 asyncPreempt
  5. 恢复执行:M 醒来执行 asyncPreempt
  6. 让出 CPU:保存现场 -> 放入队列 -> 触发调度。

4. 特殊情况与限制

虽然是“异步抢占”,但并非任何时刻都能抢占。Go 运行时定义了安全点(Safe Points)
如果 G 正处于以下情况,抢占可能会被延迟或忽略:

  • 持有运行时锁。
  • 正在与 C 代码交互(Cgo)的某些阶段。
  • 正在进行内存分配等关键运行时操作。

这种机制完美解决了紧密循环(Tight Loop)无法被调度的问题,使得 Go 的调度器更加健壮,接近于操作系统线程的抢占体验。

00:00
00:00