GMP
Go 是从语言级别就天然支持并发的,而 GMP 则是实现之的重要模型。
GMP(Goroutine Machine Processor),其中,G 是 goroutine,是 golang 实现的协程,M 是 OS线程,P 是逻辑处理器。可以说是 GMP 体系实现了 Goroutine。
协程
先来聊聊什么是协程,我们对进程和线程的区别早已了然于心,这次就借助线程来类比一下协程。
线程:操作系统中内核控制程序调度的最小单位,对于线程的创建、销毁、切换都离不开内核的调度和指挥。
协程:协程是一种用户级线程,是一种轻量级线程,是用户对线程概念的抽象封装。其调度过程由用户态闭环完成,无须内核的介入。
而线程和协程是一对多的关系,也就是说,一个线程可以包含多个协程,而一个协程只能属于一个线程。并且协程由于其不涉及内核态切换,因此可以获得更高的执行效率和更小的资源和性能开销。
其中,Goroutine 则是 Go 语言实现的协程,由 Go 运行时管理,依附于 GMP 体系。
GMP 体系
GMP = Goroutine + Machine + Processor
G: Goroutine,协程,是 Go 语言实现的用户级线程。它在 GMP 模型中是需要被执行的任务。
M: Machine,可以理解为线程,和 G 存在绑定关系。它存在两个职能,一个是寻找 G(M 会尝试从 P 的本地队列、全局队列或者其他 P 的队列中寻找可执行的 G),一个是执行 G。
P: Processor,可以理解为处理器,是 GMP 模型中的调度核心。它控制了整个 GMP 体系的调度,将 G 分配到 M 上执行。
在 GMP 模型中,P 的数量默认等于 CPU 的核心数,这能保证在多核处理器上充分利用 CPU 资源,它关联了本地可运行任务(G)的队列。当一个 M 执行的 G 发生阻塞(例如进行 I/O 操作)时,M 会和当前的 P 分离,P 会寻找其他空闲的 M 或者创建一个新的 M 来继续执行队列中的 G;当阻塞的 G 恢复后,它会被重新放入某个 P 的队列中等待执行。
调度流程
- 创建 Goroutine:当在代码中使用 go 关键字创建一个新的 Goroutine 时,这个新的 Goroutine 会被放入创建它的 P 的本地队列中。如果本地队列已满,则会将部分 Goroutine 移动到全局队列中。
- M 寻找可执行的 G:一个 M 会首先尝试从其关联的 P 的本地队列中获取一个 Goroutine。如果本地队列为空,M 会尝试从全局队列中获取 Goroutine。如果全局队列也为空,M 会尝试从其他 P 的本地队列中 “偷取” 一半的 Goroutine 到自己的 P 的本地队列中。
- 执行 G:一旦 M 找到一个可执行的 Goroutine,它就会执行该 Goroutine 的代码。在执行过程中,Goroutine 可能会因为某些操作(如 I/O 操作、系统调用等)而阻塞。
- 阻塞处理:当一个 Goroutine 发生阻塞时,关联的 M 会和当前的 P 分离,P 会寻找其他空闲的 M 或者创建一个新的 M 来继续执行队列中的 Goroutine。当阻塞的 Goroutine 恢复后,它会被重新放入某个 P 的队列中等待执行。
总结一下 M 获取任务,也就是 G 的流程:
- 先在本地队列找:M 优先从关联的 P 的本地队列中获取 Goroutine,这样可以减少锁的竞争,提高调度效率。
- 再去全局队列:当本地队列为空时,M 会从全局队列中获取 Goroutine,确保全局队列中的 Goroutine 也能得到执行。
- 工作窃取:如果全局队列也为空,M 会尝试从其他 P 的本地队列中 “偷取” Goroutine,从而实现负载均衡。