当前位置: 首页 > news >正文

深入 Go 底层原理(三):Goroutine 的调度策略

1. 引言

Goroutine 是 Go 语言并发的基石,它常被称为“轻量级线程”。与操作系统(OS)线程相比,Goroutine 的创建和销毁成本极低,上下文切换也快得多。这得益于 Go 语言在 runtime 中实现的一套高效的用户态调度系统。

本文将宏观地介绍 Goroutine 的调度策略,包括其 M:N 模型、工作窃取(Work-Stealing)和抢占机制,为下一篇深入 GMP 模型打下基础。

2. M:N 调度模型

Go 的调度器采用的是 M:N 模型

  • M (Machine): 代表内核线程(OS Thread)。

  • N (Goroutine): 代表 Go 语言的用户态协程。

M:N 模型意味着,调度器会将 N 个 Goroutine 动态地、多路复用地调度到 M 个内核线程上执行。通常情况下,M 的数量远小于 N 的数量(M 约等于 CPU 核心数)。

这种模型的优势在于:

  • 低成本切换:Goroutine 的上下文切换完全在用户态完成,不涉及内核态和用户态的转换,成本非常低(只需保存几个寄存器,如 PC, SP)。

  • 高效利用资源:当一个 Goroutine 因系统调用(如 I/O)或 channel 操作而阻塞时,调度器会将其从内核线程 M 上摘下,并让 M 去执行另一个可运行的 Goroutine,从而避免了线程的空闲和浪费。

3. 核心调度策略:工作窃取 (Work-Stealing)

为了让所有内核线程都尽可能地“忙碌”起来,Go 调度器采用了一种非常有效的负载均衡策略——工作窃取

在 GMP 模型中(详见下篇),每个处理器 P (Processor) 都有一个自己的本地可运行 Goroutine 队列(Local Run Queue, LRQ)。调度流程如下:

  1. 优先本地队列:当一个内核线程 M 准备执行 Goroutine 时,它会优先从其绑定的 P 的本地队列中获取 G。这个过程不需要加锁,非常高效。

  2. 尝试全局队列:如果 P 的本地队列为空,M 会尝试从全局可运行队列(Global Run Queue, GRQ) 中获取一批 G 到自己的本地队列。访问全局队列需要加锁。

  3. 工作窃取:如果全局队列也为空,M 将会变身为一个“小偷”,它会随机地选择另一个处理器 P',并尝试从 P' 的本地队列中**“窃取”**一半的 Goroutine 到自己的本地队列中。

工作窃取机制极大地提高了调度器的效率和并行度,确保了当一个 P 的任务繁重时,空闲的 P 可以主动过来分担,实现了任务的动态负载均衡。

4. Goroutine 的抢占 (Preemption)

如果一个 Goroutine 长时间占用一个线程 M(例如,进行密集的计算),其他 Goroutine 就会“饿死”。为了防止这种情况,Go 需要一种抢占机制,让出 CPU 给其他 Goroutine。

Go 的抢占经历了两个阶段:

  1. 协作式抢占 (Go 1.14 之前)

    • 编译器在函数调用的入口处插入一些“抢占检查”代码。

    • 当 Goroutine 进行函数调用时,会检查一个全局的抢占标记。如果标记被设置,它会主动让出 CPU。

    • 缺点:如果一个 Goroutine 只是在一个没有函数调用的 for 循环中进行密集计算,它将永远不会被抢占。

  2. 基于信号的异步抢占 (Go 1.14 及以后)

    • Go runtime 会启动一个名为 sysmon 的监控线程。

    • sysmon 会定期检查所有正在运行的 Goroutine。如果发现某个 G 运行时间超过了一个阈值(如 10ms),它会向该 G 所在的线程 M 发送一个抢占信号

    • M 接收到信号后,会中断当前 G 的执行,将其重新放回队列,然后执行其他 G

这种异步抢占机制解决了协作式抢占的痛点,保证了即使在没有函数调用的“死循环”计算中,Goroutine 也能被公平地调度。

5. 总结

Go 语言的 Goroutine 调度器是一个设计精良、高效的系统。它通过 M:N 模型实现了 Goroutine 与内核线程的解耦,通过工作窃取策略实现了负载均衡,并通过异步抢占机制保证了调度的公平性。这些策略共同构成了 Go 高并发性能的基石。下一篇,我们将深入构成这一切的 GMP 三大组件。

http://www.dtcms.com/a/311857.html

相关文章:

  • OSPF综合
  • VS Code高效开发指南:快捷键与配置优化详解
  • 深入 Go 底层原理(十二):map 的实现与哈希冲突
  • Mybatis学习之获取参数值(四)
  • 字符串(java不死)
  • c++之基础B(进制转换)(第三课)
  • 详解Python标准库之并发执行
  • AI Agent开发学习系列 - LangGraph(3): 有多个输入的Graph
  • C#多数据库批量执行脚本工具
  • OneCode3.0 核心表达式技术深度剖析:从架构设计到动态扩展
  • 波士顿咨询校招面试轮次及应对策略解析
  • 双机并联无功环流抑制虚拟阻抗VSG控制【simulink仿真模型实现】
  • OneCodeServer 架构深度解析:从组件设计到运行时机制
  • 「iOS」————weak底层原理
  • Conda创建虚拟环境,解决不同项目的冲突!
  • Windows本地使用dify搭建知识库+ollama+deepseek
  • 从零打造大语言模型--处理文本数据
  • vue引入阿里巴巴矢量图库的方式
  • SpringBoot3.x入门到精通系列: 2.3 Web开发基础
  • sifu mod制作 相关经验
  • 11:java学习笔记:1D array(1维数组)
  • Windows下定位Mingw编译的Qt程序崩溃堆栈
  • Python科研数据可视化技术
  • 2025年常见网络安全问题及针对性预防措施
  • 小迪23年-22~27——php简单回顾(2)
  • pytorch的 Size[3] 和 Size[3,1] 区别
  • 动态规划Day7学习心得
  • 深入理解Linux线程:从概念到控制的最佳实践
  • jenkins从入门到精通-P1—九五小庞
  • Python编程基础与实践:Python函数编程入门