当前位置: 首页 > 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.xdnf.cn/news/1231561.html

相关文章:

  • [论文阅读] 人工智能 + 软件工程 | GitHub Marketplace中CI Actions的功能冗余与演化规律研究
  • Text2SQL:如何通过自然语言直接获取数据,打破技术壁垒?
  • 【Android】通知
  • Docker 的网络模式
  • 红黑树(RBTree)
  • 【LeetCode 热题 100】(四)子串
  • 前端-移动Web-day3
  • 云环境K8s集群WebSocket连接失败解决方案
  • 【REACT18.x】使用vite创建的项目无法启动,报错TypeError: crypto.hash is not a function解决方法
  • 基于 LightGBM 的二手车价格预测
  • GaussDB having 的用法
  • 图像加密学习日志————论文学习DAY4
  • 分布式事务----spring操作多个数据库,事务以及事务回滚还有用吗
  • 机械臂的轨迹生成的多种方案
  • Jupyter notebook如何显示行号?
  • MFC 实现托盘图标菜单图标功能
  • NCV8402ASTT1G自保护N沟道功率MOSFET安森美/ONSEMI 过流过温保护汽车级驱动NCV8402ASTT1
  • 从基础功能到自主决策, Agent 开发进阶路怎么走?
  • 【计算机网络】Socket网络编程
  • Android 15 限制APK包手动安装但不限制自升级的实现方案
  • 断路器瞬时跳闸曲线数据获取方式
  • Javaweb————Apache Tomcat服务器介绍及Windows,Linux,MAC三种系统搭建Apache Tomcat
  • 嵌入式第十八课!!数据结构篇入门及单向链表
  • Oracle 11gR2 Clusterware应知应会
  • IDM下载失败排查
  • 704. 二分查找
  • 市政污水厂变频器联网改造方案-profibus转ethernet ip网关(通俗版)
  • CommonJS和ES6 Modules区别
  • python:以支持向量机(SVM)为例,通过调整正则化参数C和核函数类型来控制欠拟合和过拟合
  • Autosar Nm-网管报文PNC停发后无法休眠问题排查