Go 是如何做抢占式调度的?

一、什么是"抢占式调度"?

先理解什么是"抢占"。

非抢占式(早期 Go)

早期 Go(1.14 之前):

  • goroutine 只有在

    • 函数调用
    • channel 操作
    • syscall
    • runtime 检查点

才会被切换。

如果一个 goroutine 写成:

go 复制代码
for {
}

会发生什么?

👉 它永远不会让出 CPU

👉 其他 goroutine 全部饿死

这叫:

协作式调度(主动让出)


抢占式调度

抢占式调度是:

即使 goroutine 不愿意让出 CPU,调度器也可以强制打断它

这就是现代 Go(1.14+)的机制。


二、Go 现在是如何"强制打断"的?

核心答案:

通过"信号 + 栈检查"实现异步抢占

我们拆开讲。


三、抢占的触发条件

Go 会给每个运行中的 goroutine 设置一个"时间片"。

大概:

复制代码
10ms 左右

如果:

某个 G 连续运行超过时间片

调度器就会尝试抢占它。


四、真正的技术实现(关键来了)

Go 通过:

复制代码
发送操作系统信号(SIGURG)

去打断正在运行的线程 M。

步骤如下:


第一步:标记需要抢占

调度器发现:

这个 G 运行太久了

就会:

go 复制代码
g.preempt = true

标记它需要被抢占。


第二步:给 M 发信号

Go runtime 会向正在运行的 M 发送一个系统信号:

复制代码
SIGURG

这会打断线程执行。


第三步:信号处理函数触发

当 M 收到信号时:

会执行 Go runtime 的信号处理函数。

这个函数会:

  1. 保存当前寄存器状态
  2. 检查是否可以安全抢占
  3. 修改程序计数器
  4. 跳转到调度函数

五、什么叫"安全抢占点"?

Go 不能在任何地方强行打断。

比如:

  • 正在修改栈
  • 正在执行 runtime 内部关键代码
  • 正在 GC 关键阶段

所以必须在"安全点"抢占。

Go 在函数调用前后插入:

复制代码
栈边界检查

类似:

asm 复制代码
CMP SP, stackGuard

如果发现:

  • 栈溢出
  • 或被标记需要抢占

就跳到 runtime。

这就实现了:

在几乎所有函数调用处都可被抢占


六、为什么 1.14 是重大升级?

1.14 之前:

  • 只能在函数调用点抢占
  • 死循环没函数调用就无法抢占

1.14 之后:

  • 加入"异步抢占"
  • 即使死循环也可以被打断

例如:

go 复制代码
for {
}

现在也会被强制暂停。


七、完整抢占流程(一步一步)

假设:

复制代码
G1 正在运行

1️⃣ G1 运行超过 10ms

2️⃣ 调度器标记 G1 需要抢占

3️⃣ 给 M 发送信号

4️⃣ M 收到信号

5️⃣ 保存当前执行现场

6️⃣ 切换到 runtime 调度代码

7️⃣ G1 状态改为 runnable

8️⃣ 放回队列

9️⃣ 执行其他 G


八、和操作系统抢占有什么区别?

操作系统线程调度:

  • 由内核控制
  • 时间片强制切换线程

Go goroutine 调度:

  • 用户态调度
  • 通过信号模拟"线程级抢占"
  • 更轻量

九、为什么这样设计?

目标是:

  • 防止 goroutine 独占 CPU
  • 保证公平性
  • 提高多核利用率
  • 让 GC 可以安全暂停世界

尤其是 GC:

如果不能抢占 goroutine:

复制代码
STW(Stop The World)会卡死

十、和 GC 的关系

Go 的 GC 需要:

在安全点暂停所有 goroutine

抢占机制保证:

  • 即使 goroutine 死循环
  • GC 也能暂停它

这非常关键。


十一、核心总结

一句话版本:

Go 通过向线程发送信号,在安全点打断 goroutine,实现异步抢占式调度。

再浓缩:

复制代码
时间片到
→ 标记抢占
→ 发信号
→ 跳到调度器
→ 切换 goroutine

十二、一个形象比喻

想象:

  • G 是学生
  • M 是老师
  • P 是教室

以前:

学生必须自己举手说"我讲完了"。

现在:

老师可以直接打断学生:

"时间到了,换人!"

这就是抢占式调度。

相关推荐
点光1 天前
使用Sentinel作为Spring Boot应用限流组件
后端
不要秃头啊1 天前
别再谈提效了:AI 时代的开发范式本质变了
前端·后端·程序员
有志1 天前
Java 项目添加慢 SQL 查询工具实践
后端
山佳的山1 天前
KingbaseES 共享锁(SHARE)与排他锁(EXCLUSIVE)详解及测试复现
后端
Leo8991 天前
rust 从零单排 之 一战到底
后端
程序员清风1 天前
程序员兼职必看:靠谱软件外包平台挑选指南与避坑清单!
java·后端·面试
鱼人1 天前
MySQL 实战入门:从“增删改查”到“高效查询”的核心指南
后端
大鹏19881 天前
告别 Session:Spring Boot 实现 JWT 无状态登录认证全攻略
后端
Java编程爱好者1 天前
从 AQS 到 ReentrantLock:搞懂同步队列与条件队列,这一篇就够了
后端
鱼人1 天前
Nginx 全能指南:从反向代理到负载均衡,一篇打通任督二脉
后端