golang 协程理解

golang协程核心调度机制是基于 GMP模型。

G是Goroutine

  • 是轻量级的、由 Go 运行时 (Go Runtime) 管理的并发执行单元,是golang独有的概念,包含了自己的栈、指令指针和状态。
  • 启动时一般只需要2kb的栈空间,创建和销毁成本极低,并且完全由代码控制,而非操作系统,G数量理论上只受内存的影响,使用及其简便。

M是Machine

  • 可以理解为操作系统的线程,与物理cpu绑定,真正执行指令的单元。
  • golang在运行时,默认会按照cpu核心数启动相应数量的M,而在实际开发中开发者可以通过参数GOMAXPROCS进行控制。

P是Processor,也是context:

  • 代表一个逻辑处理器,是CPU内核支持的虚拟处理单元,是M执行G的桥梁,是P获取可运行的G分配给M。
  • 它维护了一个本地可运行 Goroutine 队列 。
  • P的数量决定了同时可以有多少个 Goroutine 真正并行执行(因为一个 P 需要绑定到一个 M 上才能运行 G)。P 的数量默认等于 GOMAXPROCS 的值。
  • 每个 P 有一个本地可运行 Goroutine 队列 (LRQ - Local Runnable Queue)。

理解 GMP 的关键在于它们之间的动态协作关系:

一、基本执行流程:

  • 一个 P 必须绑定到一个 M 上才能构成一个有效的执行单元。
  • P 从自己的本地可运行队列中 中获取一个状态为 Runnable 的 G。
  • M 开始执行这个 G 的代码。
  • G 执行完毕后,P 会继续从 LRQ 获取下一个 G 来执行。

二、队列:

  • LRQ (Local Runnable Queue): 每个 P 都有一个自己的本地队列,存放待运行的 G。访问 LRQ 不需要加锁(或使用非常轻量级的锁),效率很高。新创建的 G 通常优先放入当前 P 的 LRQ。
  • GRQ (Global Runnable Queue): 还有一个全局的可运行 G 队列。当 G 没有明确的 P 归属(例如从网络回调中唤醒)或 P 的 LRQ 满了,G 可能会被放入 GRQ。P 会定期检查 GRQ。访问 GRQ 需要加锁,成本相对较高。

三、创建 Goroutine (go func()): 当执行 go 关键字创建一个新的 Goroutine 时,这个新的 G 对象通常会被优先放入当前 P 的 LRQ。 如果当前 P 的 LRQ 已满,则可能会将当前 LRQ 的一半 G 和这个新的 G 一起放入 GRQ。

四、G 的阻塞 (关键!):

1、系统调用 (Syscall): 当一个 G 执行了一个可能阻塞的系统调用(如文件读写、网络 IO 等):

  • G 的状态变为 Waiting。
  • 当前 M 会与 P 解绑。
  • P 会尝试寻找一个空闲的 M(或者如果 M 数量没达到上限,就创建一个新的 M)来绑定,然后继续执行 LRQ 中的其他 G。
  • 执行系统调用的 M 则会阻塞在内核态,等待系统调用完成。
  • 系统调用完成时:
    • 阻塞的 G 会被唤醒,状态变为 Runnable。
    • 这个 M 会尝试重新获取一个 P。
    • 如果 M 成功获取到一个 P(可能是它之前解绑的那个 P,也可能是其他的空闲 P),它就会继续执行这个刚唤醒的 G。
    • 如果 M 获取 P 失败(比如 P 的数量已经达到 GOMAXPROCS 且都被占用了),这个 G 会被放入 GRQ,等待其他 P 来调度。这个 M 可能会进入休眠(被 Park)。

2、Channel 操作 / Mutex 阻塞:

  • 当 G 因为读写 Channel 或等待 Mutex 而阻塞时,G 的状态变为 Waiting。
  • G 会被移出当前的 M 和 P 的执行上下文,通常会被放入与该 Channel 或 Mutex 相关联的等待队列中。
  • P 会立即从 LRQ 中选择下一个 Runnable 的 G,让当前的 M 继续执行,M 不会阻塞,P 也不需要解绑。
  • 当 Channel 可读/写或 Mutex 被释放时,等待队列中的 G 会被唤醒,状态变为 Runnable,并被放入某个 P 的 LRQ(通常是唤醒它的那个 G 所在的 P 的 LRQ 或 GRQ),等待调度执行。

五、Work Stealing (工作窃取):

  • 当一个 P 的 LRQ 为空时,它不会立刻闲置。
  • 它会先尝试从 GRQ 获取 G。
  • 如果 GRQ 也为空,它会随机选择另一个 P,并尝试从那个 P 的 LRQ "窃取" 一半的可运行 G 到自己的 LRQ 中来执行。
  • Work Stealing 机制极大地提高了调度效率和 CPU 利用率,实现了 Goroutine 在 P 之间的负载均衡。

六、M 的生命周期 (线程管理):

  • Go Runtime 会维护一个 M 的池子。
  • 当 P 需要 M 但没有空闲 M 时,且 M 数量未达上限,会创建新的 M。
  • 当 M 长时间没有 G 可运行(并且没有阻塞在 syscall 中),它可能会被 Runtime 回收或置于休眠状态(Park),以节省系统资源。
  • 有专门的系统监控线程 (Sysmon) 负责 M 的创建、休眠、唤醒等管理工作。

七、Sysmon (系统监控线程):

  • 这是一个独立于 GMP 模型的、由 Runtime 启动的 M(不占用 P),它在后台运行,执行一些关键的维护任务:
  • 垃圾回收 (GC) 触发: 判断是否需要进行 GC。
  • 调度器抢占 (Preemption): 检测运行时间过长的 G(防止饿死其他 G),并向其发送信号,使其在安全的点(如函数调用)暂停,让出 M 给其他 G。(这是 Go 1.14 之后引入的基于信号的抢占,使得调度更公平)。
  • 网络轮询 (Netpoller): 处理网络 IO 事件,唤醒因网络 IO 而阻塞的 G。
  • M 的管理: 回收空闲 M,注入 M 处理阻塞 G 等。

在实际开发中,观察和调整协程的方法:

  1. 可以通过GOMAXPROCS(n): 设置或获取 P 的数量。通常设置为 CPU 核心数可以获得较好的并行性能
  2. 通过go tool trace 生成详细的执行追踪文件,可以可视化分析 Goroutine 的调度、阻塞、GC 等。
  3. 通过runtime/pprof 生成火焰图分析 CPU 使用情况、Goroutine 阻塞情况等,帮助定位性能瓶颈和调度问题
相关推荐
梦想很大很大14 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰19 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想