GMP 模型分析

​ 关于 GMP 模型的一些理解,G 为 goroutine, M 为 thread(内核级线程)、P 为 Processor(处理器)。

1、Golang 早期调度器的由来

​ 在讲述早期调度器之前,让我们先聊一下早期的操作系统。

​ 在没有多核 CPU 之前,操作系统以单进程的任务执行,计算机在只能一个任务一个任务完整的执行的情况。在此情况下,操作系统不仅存在工作效率低下的问题,而且一旦正在执行任务被阻塞时,CPU 资源无法释放,会导致其 CPU 资源与时间的浪费,因此操作系统采用了时间片轮询的方式调度进程,而此举动也无法改变单核 CPU 的硬件条件。

​ 为了解决上述缺陷,随后引入了多进程、多线程的解决方式。虽然多进程与多线程的方式很好解决了 CPU 调度的,效率问题,但是设计多进程、多线程的架构会变得异常复杂。不仅如此,当进程与线程数量越多的时候,多进程多线程系统进行进程线程切换的成本就越大,资源浪费现象也越明显:如(锁、竞争资源冲突等),所以在多进程、多线程模型中也存在相应的壁垒,即高内存占用与高 CPU 调度消耗。

​ 而 Go 语言为了更好的解决操作系统中线程调度的开销大,引入了比线程更轻量级的协程,其内存占用一般只有 4KB ,调度灵活,切换成本低。并且开发出 Go 语言早期的调度器:基本的全局 Go 队列和比较传统的轮询方法,利用多个 M 进行 G 的调度,可以称其为 GM 模型。

2、GM 早期调度器的缺点

​ Go 语言早期的调度器 GM 模型虽然能够利用多核的 CPU ,但是其相应的缺点也非常明显:

  • 1、创建、销毁、调度 G 都需要每个 M 获取锁,形成了激烈的锁竞争
  • 2、M 调度 G 时,会造成延迟以及额外的系统负载
  • 3、系统调用(CPU 在 M 之间的切换)导致频繁的线程阻塞和取消操作,增加了系统开销。

​ 所以为了解决以上的问题,引入了 GMP 模型。

3、GMP 现代调度器简介

​ 如下图可知,GMP 现代调度器,采用了两种队列:全局队列以及本地队列。

全局队列很好理解,可视为全局变量,本地队列则能理解为局部变量。本地队列的个数依赖于 P 的个数,即 GOMAXPROCS 的个数,此值由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。每一个 P 的本地队列中能存放 G 的个数不超过 256 个。新建的 G 一般优先放置在 P 的本地队列中。

​ Go 语言本身限定 M 数量的最大量为 10000 (忽略),一般操作系统也达不到 10000 个线程。使用 runtime/debug 包中的SetMaxThreads 函数来设置。有一个 M 阻塞,就会创建一个新的 M,如果有 M 空闲,则会进行回收或者睡眠。

4、GMP 调度策略

​ 设计策略有以下四种:

1、线程复用

​ 采用两种机制:work stealing 机制 与 hand off 机制。

​ work stealing 机制:当本线程无可运行的 G 时,尝试从其他的线程绑定的 P 偷取 G,而不是销毁线程,优先级是先从全局队列中获取 G,再从其他的 P 的本地队列中获取 G 。

​ hand off 机制:当本线程因为 G 进行系统调用阻塞的时候,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

2、并行应用

​ 一般来说,GOMAXPROCS 限定的 P 个数为 CPU 的核心数量的一半。

3、抢占

​ 有多个 G 等待执行时候,每个 G 在 cpu 执行下不超过 10ms,防止其他 G 被饿死现象。

4、全局 G 队列

​ 基于work stealing 机制进行的补充。从其他 P 本地队列偷不到 G 时,偷取全局队列的 G 。


GMP 模型分析
https://chaggle.github.io/2022/02/24/go/concurrency/GMP/
作者
chaggle
发布于
2022年2月24日
许可协议