【4】为什么Go能挂住成千上万个goroutine,线程却没爆?一次讲透GMP调度模型

news2026/5/24 10:53:28
如果你写 Go 写的久了很容易对一件事习以为常请求来了起一个goroutine后台任务想并发跑再起几个goroutine网络连接一多程序里挂着成千上万个goroutine好像也不算什么稀奇事。代码写起来很顺读起来也像普通的阻塞程序。可只要停下来追问一句就会发现这件事并没有看上去那么理所当然。如果这些执行路径都在并发往前推进为什么线程数没有跟着一起膨胀如果线程总会碰到阻塞系统调用、网络等待和 CPU 争抢程序为什么又没有轻易被拖垮再往前追一步如果某个goroutine一直不肯让出 CPU或者某个线程正卡在系统调用里调度器到底靠什么把其他工作继续推下去很多介绍Go并发的材料到了这里会先给一个很快的答案goroutine是轻量线程。这个说法不能算错但只说到了表面。真正难的部分不在“它比线程轻”这件事上而在它为什么能轻这种轻量在阻塞、调度和并行度控制面前为什么还站得住Go运行时要解决的不是简单地把线程包装得更便宜一点而是让大量待执行的工作在有限的线程和有限的并行度上稳定流动。真正撑起这套并发模型的是G、M、P三层分工围绕它们建立起来的队列、让渡、窃取和恢复机制把这几层关系摆正后面再看GOMAXPROCS、netpoll、抢占调度和runtime/proc.go就不会觉得它们像一堆零散技巧。先抓住四个最容易卡人的问题G、M、P三个角色分别是什么为什么偏偏要拆成这三层。线程真的阻塞时为什么整个调度系统没有被一起拖住。调度器平时是按什么顺序找任务的为什么它不靠一个全局大队列解决一切。网络 I/O 明明写得像阻塞代码为什么底层又不需要一连接一线程地死等。把这四层关系先看稳**GMP的主骨架就差不多立住了。**后面的抢占、公平性和版本细节都只是沿着这副骨架继续展开。真正要看清的不是goroutine比线程“轻”多少而是Go运行时到底怎样让大量工作在有限资源上稳定流动。先把G、M、P的角色摆正为什么只把goroutine叫轻量线程还不够把goroutine只理解成轻量线程往往还是不够。真正需要补上的是这种“轻”靠什么维持。Go调度器没有把并发执行这件事直接压到线程身上而是拆成了三层G负责承载待执行的工作M负责把代码真正放到操作系统线程上运行P负责提供执行Go用户代码时所必需的调度上下文也正因为多了P这一层运行时才有办法一边限制同一时刻的并行度一边处理大量goroutine与少量线程之间的映射关系。图里把本地队列、全局队列和工作窃取一起画出来就是为了先把这套分工的骨架立住。图示先看清G、M、P的分工以及本地队列、全局队列和工作窃取如何连在一起。关键不是一对一映射而是三层分工关键不在“一个goroutine配一条线程”而在怎样让大量G在有限的M和P上有序流动。G是工作M是执行者P是执行资格和调度上下文。先把这三层拆开后面再看阻塞、窃取、抢占这些动态过程时就不容易把它们误解成某种“线程池技巧”。G负责工作M负责执行P负责让执行这件事真正成立。角色分工先立住之后下一个问题就是这套结构一旦遇到现实里的阻塞会不会立刻失灵。线程既然是真的线程总会碰到系统调用而只要线程会卡住调度器就必须回答谁继续往前跑。阻塞发生时为什么系统还能继续往前跑系统调用阻塞是理解GMP时最容易卡住的一步。直觉上看只要某条执行路径进入阻塞调用承载它的线程就停住了而线程一旦停住整个并发系统似乎也会跟着受影响。Go调度器真正解决的恰恰是**“线程会阻塞”带来的连锁问题**。关键做法不是让线程永远不阻塞而是当某个M因系统调用暂时卡住时把它手里的P及时让出去让别的线程接手继续执行其他 runnableG。这样阻塞就被限制在局部不会扩散成整片调度停摆。图示某个M因阻塞系统调用停住后P如何让出并由其他线程接手继续推进剩余的 runnableG。这里要抓住的重点不是“系统调用不贵”也不是“线程不会卡住”而是阻塞发生时调度器仍然可以把执行能力保留下来。可以把这件事压缩成两个判断真正被卡住的是那条进入系统调用的执行链不是整个Go程序的并发推进能力P的关键就在这里它把“线程是否阻塞”和“系统还能不能继续调度别的工作”拆成了两件事。被阻塞的是某条执行链不是整个调度系统。不过只知道阻塞时可以让出P还不够。调度器能继续往前跑前提是它知道下一步该去哪里找活。光理解“阻塞时怎么救场”还只是半截接下来还得补上“平时到底怎么调度”。调度器平时是按什么顺序找任务的理解GMP不能只停留在静态分工还得往前追一步当某个线程已经拿到P准备继续执行下一段Go代码时它到底先从哪里找任务。这里没有一个“全局平均分配器”在背后统一发号施令调度器走的是一条层次很明确的优先路径先看runnext再看当前P的本地队列本地没有再去碰全局队列还不够再考虑从别的P那里工作窃取最后再看netpoll有没有已经就绪、可以恢复执行的网络 I/O 任务这个顺序直接决定了调度开销、局部性和全局竞争的强弱。图示调度器为当前P寻找下一个G时的优先路径从runnext到本地队列再到全局队列、窃取和netpoll。这一步的结论可以压缩成一句话调度器优先在本地解决问题只在本地耗尽时才逐层向外找补。这样做不是为了绝对公平而是为了在下面几件事之间保持平衡吞吐响应调度成本Go调度器并不是一个一视同仁的大池子而是一套“本地优先、全局兜底、必要时再平衡”的分层机制。调度器不是平均发活而是优先本地解决缺活时再逐层向外找补。走到这里调度器的日常工作方式已经大致清楚了平时先在本地找任务必要时再向外扩展线程阻塞了也可以通过让渡P把系统继续推下去。剩下最值得单独拎出来的一类场景就是网络 I/O。它在业务代码里看起来最像老老实实堵在那里等结果但运行时内部的处理方式又和直觉差得最远。网络 I/O 为什么能写得像阻塞代码网络 I/O 是另一个很容易被表象带偏的地方。业务代码写起来像顺序阻塞调用读写接口如果数据没来就在那里等等到了再继续往下走。可如果底层真的是“一条连接占住一条线程”地等待Go就很难在高并发网络场景里维持现在这种资源利用方式。运行时真正做的是把下面两件事拆开代码写起来像阻塞底层调度不能被长期占住某个G发起网络读写后如果文件描述符暂时还没就绪运行时会先把这个G挂起把等待事件交给netpoll同时把线程和P释放回调度器继续处理别的 runnableG。等事件真正到来再把对应的G放回队列等待重新调度。图示网络 I/O 尚未就绪时等待中的G先挂起事件到来后再由netpoll唤醒并放回调度队列。真正要抓住的是网络 I/O 的“阻塞感”主要发生在G这一层而不是把整个线程长期绑死在一次等待上。对业务代码来说执行流依然是顺序的对运行时来说这其实是一条挂起等待事件重新入队恢复执行的调度链。正因为有这条链路Go才能同时保留直观的编程模型和较高的并发承载能力。对业务代码来说像阻塞对运行时来说是挂起、等待、唤醒和重新调度。把前面四张图连起来看GMP并不是四个互不相干的小技巧拼在一起而是一套围绕“怎样让大量工作稳定流动”展开的整体设计。G、M、P先把角色拆开P的让渡解决阻塞扩散本地队列和工作窃取控制日常调度netpoll再把网络等待这类高频场景接回主线。走到这里调度器的主骨架已经基本立住了。接下来还需要补上的是另外两件事如果某个goroutine一直占着 CPU 不放运行时怎么把它抢下来而GOMAXPROCS又究竟在限制什么。忙循环为什么最终还是会被抢占抢占要解决的是没有停顿点时怎么办把前面的阻塞、队列和网络等待都看清之后最后一个很自然的问题变成了纯计算场景。假设有一个goroutine什么都不等不做系统调用不碰网络 I/O也不主动调用Gosched()只是一直埋头跑计算。按最粗糙的直觉看只要它一直不让别的任务是不是就只能一直等着如果真是这样前面讲的那些机制就只对“愿意停下来”的任务有效调度器的公平性会非常脆弱。只要有一个忙循环足够长其他 runnablegoroutine就会被压在后面垃圾回收和调度延迟也会一起变差。Go运行时不能把公平性建立在“业务代码最好自觉一点”这件事上所以它必须再补一层能力某个goroutine长时间占着 CPU 不放时运行时要能主动把它拉下来。这就是抢占要解决的问题。它针对的不是“阻塞了怎么办”而是**“根本没有阻塞也没有自愿让出时怎么办”。**前面几节讲的是调度器如何利用停顿点继续往前推这一节讲的则是当停顿点迟迟不出现时运行时如何自己制造一个。早期的Go调度更接近协作式思路运行中的goroutine往往要在函数调用、通道操作、系统调用这类位置自然交回执行权。只要代码路径里有这些点问题并不大可一旦遇到长时间运行的纯计算循环这种做法就会变得被动。后来运行时把抢占能力补强了核心思路也不复杂调度器不会无限期等某个goroutine自己让出 CPU而是会在它占用 CPU 过久时想办法把“该停一下了”这件事递到它当前的执行路径上。抢占不是从任意位置硬切走这里最容易混淆的地方是把抢占理解成“立刻从任何指令位置硬切走”。实际并不是这个意思。运行时仍然要考虑安全边界尤其要避免在不合适的位置把栈、寄存器和执行状态切乱。更贴近事实的说法是某个goroutine运行得太久时运行时会给它打上“应该被抢占”的标记并尽快把执行流引到合适的安全点让调度器重新接管对业务代码来说外部观感是“忙循环最终还是会被切下来”对运行时来说这件事仍然是在可控边界内完成的。把这层机制放回前面的主线里看调度器其实一直在做两件互补的事。只要某个goroutine因阻塞、等待或主动交回执行权而停下来就立刻利用这个空档安排别的工作如果某个goroutine长时间不停那就不能一直等它主动停下来而要由运行时自己推动一次切换。前者解决“有空档时怎么利用”后者解决“迟迟没有空档时怎么办”。看到这里GMP的日常运转逻辑已经比较完整了。再往下就该收束到另一个经常被说错的点了。很多人一提GOMAXPROCS第一反应就是“线程数上限”或者“最多开多少个并发任务”。这两个说法都不准确。真正决定同时能跑多少段Go代码的不是G的数量M的总数而是P的数量。GOMAXPROCS限制的不是goroutine数也不是线程总数它先不是任务总数和线程总数GOMAXPROCS这个名字很容易把人带偏。它看上去像某种“最大线程数”设置再加上平时又常和 CPU 核心数一起出现于是很多人顺手就把它理解成“线程池大小”或者“最多能开多少个并发任务”。可如果把前面的GMP关系图重新拿回来对照这个解释很快就会露出问题。先看最直接的一层GOMAXPROCS并不限制goroutine的数量。程序里完全可以同时存在几十万、几百万个goroutine其中绝大多数都只是处在等待、可运行或者未来某个时刻才会继续推进的状态。Go运行时并不会因为GOMAXPROCS8就只允许存在 8 个goroutine这两件事根本不在一个维度上。可以这样对照理解goroutine的数量对应的是系统里一共有多少份待处理工作GOMAXPROCS管的是同一时刻最多允许多少份Go用户代码并行执行再看第二层GOMAXPROCS也不等于线程总数上限。线程是M这一层的资源而M会因为系统调用阻塞、网络轮询、运行时后台任务等原因出现增减。某个时刻程序里实际存在的线程数可能比GOMAXPROCS大也可能远小于它。只要这些线程里有一部分正在阻塞运行时就可能需要额外的M去接手剩余的P把调度继续往前推。所以拿“线程总数”去理解GOMAXPROCS很容易把系统调用阻塞这类关键场景直接抹平。它真正限制的是可用并行度真正更准确的说法是GOMAXPROCS限制的是可用并行度。也就是同一时刻最多有多少个P可以让普通Go代码并行执行。因为每个P都像一张“执行许可证”M拿到P之后才能真正跑用户态Go代码所以P的数量才是那条决定并行度的硬边界。程序里可以有很多G在排队也可以有额外的M因阻塞和唤醒在系统里流动但真正同时向前推进的Go代码段数最终还是被P的数量卡住。GOMAXPROCS限制的不是任务总数也不是线程总数而是同一时刻能并行执行多少段Go代码。这样再回头看一个很常见的场景很多误解就能自动消掉。假设机器有 8 个逻辑 CPU而GOMAXPROCS也设成 8。即使程序里有一千个 runnablegoroutine同一时刻也只会有 8 份Go代码真正并行执行剩下的都要等待调度反过来即使系统里因为阻塞系统调用存在超过 8 条线程这也不代表有超过 8 段用户态Go代码在一起跑。线程可以多等待中的工作也可以更多但并行执行普通Go代码的“闸门”仍然在P这里。这也是为什么官方后来更明确地把GOMAXPROCS解释成 available parallelism。这个说法比“最大线程数”准确得多因为它直接点出了运行时真正要控制的对象不是线程总量不是任务总量而是并行推进用户态Go代码的能力上限。只要把这个口径放稳很多表面上绕来绕去的现象都会顺下来。goroutine可以很多因为待处理工作可以很多线程可以比GOMAXPROCS更多因为阻塞和后台任务会让运行时需要额外的M但真正同时跑起来的那一层始终要经过P。抢占和GOMAXPROCS要放在一起看把抢占和GOMAXPROCS放在一起看会发现它们补的是同一件事的两面GOMAXPROCS给并行度画出边界保证同一时刻最多能跑多少抢占保证在这条边界之内不会因为某个忙循环长期霸占执行权让别的任务永远挤不进来一个负责把入口收住一个负责让入口内部保持流动。正因为这两层同时存在Go调度器才不是“谁抢到线程谁一直跑”的粗放模型而是一套既限制并行度、又维持公平推进的运行时机制。再回头看开头那个最直观的现象答案其实已经很清楚了。程序里可以挂着成千上万个goroutine却不需要把线程数一起撑大不是因为运行时偷偷做了什么难以名状的优化而是因为它从一开始就没有把下面三件事混成一层工作线程并行执行资格G、M、P把角色拆开队列和窃取让工作流动起来P的让渡把阻塞控制在局部netpoll把等待中的网络 I/O 接回调度主线抢占和GOMAXPROCS则把公平性和并行度边界一起收住。走到这里GMP就不再只是三个缩写而是一套能解释日常Go并发行为的具体运行机制。Go的并发之所以能“看起来简单、跑起来不乱”靠的不是口号式的轻量线程而是G、M、P和整套调度机制一起工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2557923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…