GPM 模型
前言
-
Go 调度器模型我们通常叫做G-P-M 模型
-
G、P、M 是 Go 调度器的三个核心组件,是 Go 语言天然支持高并发的内在动力
这里有几个概念
线程
- 操作系统最小调度单元,通常语义中的线程指的是内核级线程
- 创建、销毁、调度交由内核完成,CPU 需完成用户态与内核态间的切换
- 可以更好地使用多道程序并发执行,提高资源利用率和系统吞吐量
协程
- 定义上是一种用户态的轻量级线程
- 调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销
- 与线程存在映射关系,为 M:1
- 因为线程是最小调度单元,在内核视角下根本不知道有协程这个概念,所以协程是无法进行真正意义上的并行的。且一个协程由于某些原因发生了阻塞,有可能上升至线程阻塞,因为内核只能看到线程发生问题,而看不到协程。
Goroutine
- 在 Go 语言中进行优化后的特殊“协程”
- 与线程存在映射关系,为 M:N
- 创建、销毁、调度在用户态实现,对内核透明,足够轻巧
- 可利用多个线程实现并行
- 通过调度器的擀旋,实现和线程间的动态绑定和灵活调度
- 栈空间大小可动态扩缩,因地制宜
对比
来一波雷军比较法:
| 模型 | 弱依赖内核 | 可并行 | 可应对阻塞 | 栈可动态扩缩 |
|---|---|---|---|---|
| 线程 | ❎ | ✅ | ✅ | ❎ |
| 协程 | ✅ | ❎ | ❎ | ❎ |
| Goroutine | ✅ | ✅ | ✅ | ✅ |
有主角光环的 Goroutine 毫无悬念的胜出了,下面一起来了解一下 Golang 调度 Goroutine 时的经典模型:GPM 模型。
GPM 模型
Go 调度器模型我们通常叫做 GPM 模型~~(记忆窍门:Mai Pi Gu)~~,也有叫 GMP 模型的,讲的是同一个东西。它由 Goroutine 、Processor 、Machine 、sched 组成,sched 就是 Go 的调度器,它维护有存储 M 和 G 的队列以及调度器的一些状态信息等。
G
- G 即 Goroutine ,Golang 中对协程的抽象
- G 有自己的运行栈、状态、执行的任务函数,用户通过关键字 Go 启动一个 goroutine
- G 需要绑定 P 才能执行,在 G 的视角中,P 就是它的 CPU
P
- P 即 Processor ,只是一个抽象的概念,并不是真正的物理 CPU
- 是 GPM 的中枢,借由 P 承上启下,实现 G 和 M 之间的动态结合
- 对 G 而言,P 是它的 CPU ,G 只有被 P 调度才能执行
- 对 M 而言,P 是其执行代理,为其提供必要信息的同时(可执行的 G 、内存分配情况等)还隐藏了繁杂的调度细节
- P 的数量决定了 G 最大并行数量,可由用户通过 GOMAXPROCS 进行设定(超过 CPU 核数时无意义)
M
- M 即 Machine ,是 Golang 中对协程的抽象
- M 不直接执行 G ,而是先和 P 绑定,由其实现代理
- 借由 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() 调度流程
即:如果处理器没有任务可处理,它会按以下规则来执行,直到满足某一条规则:
从本地队列获取任务 从全局队列获取任务 从网络轮询器获取任务 从其它的处理器的本地队列窃取任务
调度器的生命周期
M0
M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在栈上分配,M0 负责执行初始化操作和启动第一个 G , 在之后 M0 就和其他的 M 一样了。
G0
G0 是每次启动一个 M 都会第一个创建的 gourtine ,G0 仅用于负责调度的 G ,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0 。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0 。