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 阻塞情况等,帮助定位性能瓶颈和调度问题
相关推荐
绝了4 小时前
Go的手动内存管理方案
后端·算法·go
大鹏dapeng4 小时前
【Gone框架】强大而灵活的配置管理系统详解
后端·go·github
一个热爱生活的普通人4 小时前
使用 go 语言实现一个 LRU 缓存算法
后端·面试·go
DemonAvenger4 小时前
Go并发编程进阶:无锁数据结构与效率优化实战
分布式·架构·go
程序员爱钓鱼4 小时前
用 Go 写一个可以双人对弈的中国象棋游戏!附完整源码
游戏·go·游戏开发
云攀登者-望正茂5 小时前
如何在 Go 中创建和部署 AWS Lambda 函数
云计算·go·aws
Pandaconda15 小时前
【新人系列】Golang 入门(十五):类型断言
开发语言·后端·面试·golang·go·断言·类型
Hello.Reader16 小时前
高可靠 ZIP 压缩方案兼容 Office、PDF、TXT 和图片的二阶段回退机制
开发语言·pdf·go
nil20 小时前
一文弄懂用Go实现自己的MCP服务
llm·go·mcp
风逆21 小时前
golang goroutine核心注意事项
go