您现在的位置是:首页 > 技术教程 正文

GO GMP

admin 阅读: 2024-03-18
后台-插件-广告管理-内容页头部广告(手机)

GMP

为了解决 Go 早期多线程 M 对应多协程 G 调度器的全局锁、中心化状态带来的锁竞争导致的性能下降等问题,Go 开发者引入了处理器 P 结构,形成了当前经典的 GMP 调度模型。

  • GMP 模型是 Go 语言调度器采用的并发编程模型
  • 它包含三个重要的组件:Goroutine(G)、操作系统线程(M)和逻辑处理器(P)
  • Go 调度器:运行时在用户态提供的多个函数组成的一种机制,目的是高效地调度 G 到 M上去执行

组成

  • Goroutine (G) 是 Go 语言中轻量级的并发执行单元,类似于线程但比线程更小、更灵活。每个 goroutine 都有自己独立的堆栈和寄存器等信息,可以通过 go 关键字创建并发执行任务。
  • 逻辑处理器(P)是一个虚拟的执行单元,负责调度 goroutine 和执行 Go 代码。Go 程序中有多个 P,每个 P 可以运行多个 goroutine,因此可以实现真正的并发执行。
  • 操作系统线程(M)是实际的执行单元,负责执行 goroutine。Go 程序中通常会创建多个 M,以便在多核 CPU 上实现并发执行。

GMM

调度场景

  1. 创建 G:

    • 正在 M1 上运行的P1,有一个G1
    • G1 通过go func() 创建 G2
    • 由于局部性,G2优先放入P1的本地队列
  2. G 运行完成后:

    • M1 上的 G1 运行完成后
    • M1 上运行的 Goroutine 会切换为 G0
    • G0 从 M1 上 P1 的本地运行队列获取 G2 去执行
    • 注:这里 G0 是程序启动时的线程 M(也叫M0)的系统栈表示的 G 结构体,负责 M 上 G 的调度
  3. M 上创建的 G 个数大于本地队列长度时:

    • P 本地队列最多能存 256 个G
    • 正在 M1 上运行的 G2 要通过go func()创建 258 个G,前 256 个G 放在 P1 本地队列中
    • G2 创建了第 257 个 G(G259)时,P1 本地队列中前一半和 G259 一起打乱顺序放入全局队列,P 本地队列剩下的 G 往前移动
    • G2 创建的第 258 个 G(G260)时,放入 P 本地队列中,因为还有空间
  4. M 的自旋状态:

    • 创建新的 G 时,运行的 G 会尝试唤醒其他空闲的 M 绑定 P 去执行
    • 如果 G2 唤醒了M2,M2 绑定了一个 P2,会先运行 M2 的 G0
    • 这时 M2 没有从 P2 的本地队列中找到 G,会进入自旋状态(spinning)
    • 自旋状态的 M2 会尝试从全局 P 队列里面获取 G,放到 P2 本地队列去执行
    • 获取的数量满足公式:n = min(len(globrunqsize)/GOMAXPROCS + 1, len(localrunsize/2))
    • 含义是每个P应该从全局队列承担的 G 数量,为了提高效率,不能太多,要给其他 P 留点
  5. 任务窃取机制:

    • 自旋状态的 M 会寻找可运行的 G
    • 如果全局队列为空,则会从其他 P 偷取 G 来执行,个数是其他 P 运行队列的一半
  6. G 发生系统调用时:

    • 如果 G2 发生系统调度进入阻塞,其所在的 M1 也会阻塞:因为会进入内核状态等待系统资源
    • 和 M1 绑定的 P1 会寻找空闲的 M 执行:这是为了提高效率,不能让 P 本地队列的 G 因所在 M 进入阻塞状态而无法执行
    • 注:M1 上的 G2 如果是进入 Channel 阻塞,则该 M 不会一起进入阻塞,因为 Channel 数据传输涉及内存拷贝,不涉及系统资源等待
  7. G 退出系统调用时:

    • 如果刚才进入系统调用的 G2 解除了阻塞
    • 其所在的 M1 会寻找 P 去执行,优先找原来的 P1
    • 如果没有找到,则其上的 G2 会进入全局队列,等其他 M 获取执行,M1 进入空闲队列

基于 GMP 模型的 Go 调度器的核心思想是:

  1. 尽可能复用线程 M:

    • 避免频繁的线程创建和销毁;
  2. 利用多核并行能力:

    • 限制同时运行(不包含阻塞)的 M 线程数为 CPU 的核心数目
    • 通过设置 P 处理器的个数为 GOMAXPROCS 来保证,GOMAXPROCS 一般为 CPU 核数
    • 因为 M 和 P 是一一绑定的,没有找到 P 的 M 会放入空闲 M 列表,没有找到 M 的 P 也会放入空闲 P 列表
  3. Work Stealing 任务窃取机制:

    • M 优先执行其所绑定的 P 的本地队列的 G
    • 如果本地队列为空,可以从全局队列获取 G 运行,也可以从其他 M 绑定的 P 中偷取 G 来运行
  4. Hand Off 交接机制:

    • M 阻塞,会将 M 上 P 的运行队列交给其他 M 执行
    • 交接效率要高,才能提高 Go 程序整体的并发度
  5. 基于协作的抢占机制:

    • 每个真正运行的G,如果不被打断,将会一直运行下去
    • 为了保证公平,防止新创建的 G 一直获取不到 M 执行造成饥饿问题
    • Go 程序会保证每个 G 运行10ms 就要让出 M,交给其他 G 去执行
  6. 基于信号的真抢占机制:

    • 尽管基于协作的抢占机制能够缓解长时间 GC 导致整个程序无法工作和大多数 Goroutine 饥饿问题
    • 但是还是有部分情况下,Go调度器有无法被抢占的情况,例如,for 循环或者垃圾回收长时间占用线程
    • 为了解决这些问题, Go1.14 引入了基于信号的抢占式调度机制,能够解决 GC 垃圾回收和栈扫描时存在的问题。

参考文章:深入分析Go1.18 GMP调度器底层原理

标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜