浅谈一波 GPM 模型

1,040 阅读4分钟

GPM 模型

前言

  • Go 调度器模型我们通常叫做G-P-M 模型

  • G、P、M 是 Go 调度器的三个核心组件,是 Go 语言天然支持高并发的内在动力

这里有几个概念

线程

  1. 操作系统最小调度单元,通常语义中的线程指的是内核级线程
  2. 创建、销毁、调度交由内核完成,CPU 需完成用户态与内核态间的切换
  3. 可以更好地使用多道程序并发执行,提高资源利用率和系统吞吐量

协程

  1. 定义上是一种用户态的轻量级线程
  2. 调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销
  3. 与线程存在映射关系,为 M:1
  4. 因为线程是最小调度单元,在内核视角下根本不知道有协程这个概念,所以协程是无法进行真正意义上的并行的。且一个协程由于某些原因发生了阻塞,有可能上升至线程阻塞,因为内核只能看到线程发生问题,而看不到协程。

Goroutine

image-20230321163759675.png

  1. 在 Go 语言中进行优化后的特殊“协程”
  2. 与线程存在映射关系,为 M:N
  3. 创建、销毁、调度在用户态实现,对内核透明,足够轻巧
  4. 可利用多个线程实现并行
  5. 通过调度器的擀旋,实现和线程间的动态绑定和灵活调度
  6. 栈空间大小可动态扩缩,因地制宜

对比

​ 来一波雷军比较法

模型弱依赖内核可并行可应对阻塞栈可动态扩缩
线程
协程
Goroutine

​ 有主角光环的 Goroutine 毫无悬念的胜出了,下面一起来了解一下 Golang 调度 Goroutine 时的经典模型:GPM 模型。

GPM 模型

​ Go 调度器模型我们通常叫做 GPM 模型~~(记忆窍门:Mai Pi Gu)~~,也有叫 GMP 模型的,讲的是同一个东西。它由 GoroutineProcessorMachine 、sched 组成,sched 就是 Go 的调度器,它维护有存储 M 和 G 的队列以及调度器的一些状态信息等。

image-20230321172452314.png

G

  1. G 即 Goroutine ,Golang 中对协程的抽象
  2. G 有自己的运行栈、状态、执行的任务函数,用户通过关键字 Go 启动一个 goroutine
  3. G 需要绑定 P 才能执行,在 G 的视角中,P 就是它的 CPU

P

  1. P 即 Processor ,只是一个抽象的概念,并不是真正的物理 CPU
  2. 是 GPM 的中枢,借由 P 承上启下,实现 G 和 M 之间的动态结合
  3. 对 G 而言,P 是它的 CPU ,G 只有被 P 调度才能执行
  4. 对 M 而言,P 是其执行代理,为其提供必要信息的同时(可执行的 G 、内存分配情况等)还隐藏了繁杂的调度细节
  5. P 的数量决定了 G 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义)

M

  1. M 即 Machine ,是 Golang 中对协程的抽象
  2. M 不直接执行 G ,而是先和 P 绑定,由其实现代理
  3. 借由 P 的存在,M 无需和 G 绑死,也无需记录 G 的状态信息,因此 G 在全生命周期中可以实现跨 M 执行

调度策略

任务窃取

​ 在实际生产中,每个 P 中的 G 执行速度有快有慢,且 G 的个数有多有少。为了充分提高 Go 的并行处理能力,当每个 P 之间的 G 任务不均衡时,调度器允许 P 从全局队列或其它 P 的本地队列中获取 G 执行。

减少阻塞

  • 由于原子、互斥量或通道操作调用导致 G 阻塞,调度器将把当前阻塞的 G 切换出去,重新调度本地队列上的其他 G
  • 由于网络请求和 IO 操作导致 G 阻塞,Go 提供了网络轮询器,通过 ipoll 实现 IO 多路复用来解决这一问题

go func() 调度流程

即:如果处理器没有任务可处理,它会按以下规则来执行,直到满足某一条规则:

从本地队列获取任务 从全局队列获取任务 从网络轮询器获取任务 从其它的处理器的本地队列窃取任务

image-20230323165625276.png

调度器的生命周期

image-20230323170946263.png

M0

M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在栈上分配,M0 负责执行初始化操作和启动第一个 G , 在之后 M0 就和其他的 M 一样了。

G0

G0 是每次启动一个 M 都会第一个创建的 gourtine ,G0 仅用于负责调度的 G ,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0 。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0 。

参考链接