图解|Go的GMP在计算密集型和IO密集型的区别

写在前面

Go的GMP我们很熟悉了,计算密集型和io密集型我们也很熟悉了,但 Go 的GMP在计算密集型和io密集型上有什么区别呢? 这篇文章我们就来探讨一下!

当然这也只是我自己的理解,如果你有不同的理解,可以评论区留言!

GMP

  • G(Goroutine):用户级轻量线程(协程),包含栈、状态、入口函数等,创建和切换成本很低,默认栈按需伸缩。
  • M(Machine):绑定到操作系统线程的运行实体,真正在CPU上执行代码。
  • P(Processor):逻辑处理器,承载调度资源(本地运行队列、定时器等),控制并行度 ,上限为 GOMAXPROCS 。

调度模型:

  • 本地队列优先:每个 P 有自己运行的本地队列,调度时优先从本地取就绪 G 。
  • 全局队列兜底:部分新就绪 G 放入全局队列,P 本地队列空时尝试取全局任务。
  • Work Stealing:本地无任务时,从随机其他 P 的队列尾部偷一半任务,均衡负载。
  • 自旋与休眠:没有可运行 G 时, M 可短暂自旋寻找任务,找不到则休眠以节省资源。

io密集型

大部分时间在等待外部资源(磁盘、网络、数据库)响应,CPU常处于空闲或低占用。

  • CPU使用率低、 iowait 高(Linux vmstat / iostat )、会存在大量阻塞/超时日志
  • 线程/连接数多、吞吐受外部系统 RT 影响

在IO密集型系统中,我们假设每一个G都是请求外部资源,比如RPC、Redis、DB等等... 当我们代码执行一次网络请求 (如http.Get) 时,GMP发生以下事情:

  1. G (Goroutine): 执行到网络读取操作,发现数据未就绪。
  2. G->Netpoller: G将自己 注册到基于epoll的网络轮询器的Netpoller中 ,状态变为waiting并与M分离。
  3. M (Machine): M不会阻塞,会立刻通过 P 获取本地队列中的下一个G继续执行。
  4. Netpoller->P: 当网络数据到达,Netpoller检测到事件,将之前的G状态改为running ,并将其注入到全局队列或某个P的本地队列。
  5. P (Processor): 在后续调度中,P再次拿到这个G,M继续执行之前的上下文。

⚠️ 注意点:M始终在工作,没有空闲。

所以对于IO密集型系统,GMP 通过挂起当前在等待数据的G,可以使得M能处理其他的G,利用等待时间,把有限的 CPU 放大成海量的并发处理能力,拆分协程收益巨大,达到四两拨千斤的效果。

计算密集型

大部分时间在执行CPU计算(算法、编码/解码、加密、ML推理),CPU长期接近满载。

  • CPU使用率高、负载 ≈ CPU核心数或更高
  • 性能对算法复杂度、指令效率、缓存命中率敏感(perf 、 pprof 显示大量CPU占用)

当我们代码执行一个死循环计算或复杂哈希时,GMP的工作就会是这样:

  1. G(Goroutine): 在 M 上疯狂消耗 CPU 周期。
  2. P(Processor): P 的本地队列里可能还有其他 G 在排队。
  3. Preemption(抢占): Go 的后台监控线程sysmon发现某个 G 运行超过 10ms,会发出抢占信号。
  4. G->Global Queue: G 检测到抢占信号,保存上下文,主动让出 CPU,回到 全局队列队尾等待 。
  • 为什么拆分成多个协程"没有那么高并发"?

在计算密集型场景下,所谓的高并发其实是假象。

  • 真并发 (Parallelism) :同一时刻真的有多个任务在物理CPU上跑,上限就是CPU核数(GOMAXPROCS)。
  • 假并发 (Concurrency) :任务在快速切换,宏观上看好像都在跑,微观上同一时刻只有几个在跑。

对于计算密集型,开一万个G,真正的并行度依然卡死在CPU核数上。反而因为过度的切换(Context Switch)和CPU缓存失效(Cache Miss),导致整体效率不如只开8个G。

所以系统CPU 算力就那么大,在计算密集型系统中,协程越多,调度损耗越多,拆分协程的目的是为了不让某一个协程独占CPU,所以在做计算密集型任务的时候不要滥用 go func() ,最好是 协程数 ≈ CPU 核数 。

相关推荐
夏幻灵6 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
程序猿编码6 小时前
榨干 RTX5090 算力!Qwen3 专用单卡推理引擎,手写C++/CUDA 算子实现 MTP 推测解码
开发语言·c++·大模型推理·qwen3·多模态推理
bkspiderx7 小时前
Qt 插件机制:动态扩展应用功能的核心框架
开发语言·qt·元数据·qt 插件·qpluginloader
程序员老陆7 小时前
深入理解 C++ thread_local:线程私有存储的正确打开方式
开发语言·c++·程序设计
沐晓时光8 小时前
C语言入门,深入理解指针(3)
c语言·开发语言
码云数智-园园8 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言
禾小西9 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
我不是阵雨9 小时前
JDK 21虚拟线程Pinning陷阱:一文拔钉解困
java·开发语言
徐小黑ACG9 小时前
Golang 基础05 结构体struct
开发语言·算法·golang
优橙教育10 小时前
零基础学AI应用开发要多久?3个月能到什么水平
服务器·开发语言·网络·php