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 。