GMP
简介
GMP是Golang语言协程调度机制的模型。
协程机制是对传统的多线程/进程机制的一种改良,虽然多进程/线程可以有效提升系统的并发能力,但是互联网等高并发场景下,需要并发处理的请求量级较大,每个请求创建一个线程会带来大量的内存等资源的消耗。
而且进程/线程切换的成本较高,对CPU产生了较大的浪费。
golang的语言的协程机制通过实现用户态线程(协程)的方式减少线程的资源消耗以及切换消耗,用户态线程在执行的时候动态绑定到系统线程上。
无论是多线程、多进程、多协程都涉及到多个任务的并发执行,而且任务数量往往多于CPU的核数,所以就有了任务调度的需求,当进程阻塞在IO、时间占用较长等场景下需要切换其他等待执行的进程。
在早期单进程时代是不需要调度器的,因为进程是串行执行的。
GMP是协程的调度机制,G代表的是go语言中的协程,M代表的是实际的线程,P代表的是Go的逻辑处理器。
协程通过调度器和系统线程进行绑定之后,才能真正执行代码逻辑,协程和线程的绑定关系可以分为1 : 1,N:1,M:N等几种模型。在Golang语言中G和P是多对一的关系,而P和M是一对一的关系。
G和P,以及P和M之间的关联关系不是固定的,例如P在运行过程中被销毁,之前绑定的G就会转移到其他的P上执行。
原理
数据存储
调度机制意味着不是所有的任务都可以得到立即执行,而不能被执行包括被切换的协程需要有存储的机制。等待被调度执行的协程存储在运行队列,Golang的调度器把队列分为局部运行队列和全局运行队列,每个和P绑定一个长度为256的循环队列,每次把G放入该队列的时候从末尾插入,获取的时候从头部获取。
除了从本地队列获取执行线程外,P还有一个特殊的字段runnext可以标识下一个要执行的协程,当该字段不为空的时候,会直接执行指向的协程,而不会从本地队列获取。
每个P关联的局部队列长度有限,所以溢出的协程都在全局队列,为了避免全局队列中的协程出现饿死现象,P每执行61次调度,就要从全局队列获取一个G到本地队列。此外等本地队列为空的时候,也会从全局队列的协程中进行调度。
全局队列的数据结构是链表,由于每个P共享全局运行队列,所以当需要从全局队列获取协程转移到本地队列的时候,先根据P的数量平分全局队列的容量,其次转移的数量不能超过局部队列容量的一半,也就是128个。
如果本地队列已经满了,那么会从本地队列中的一半转移到全局运行队列。
通过以上机制,GMP保证了协程调度的公平性。
调度时机
go语言的调度器分为三种调度类型,主动调度,被动调度,以及抢占调度。
主动调度
协程可以主动让出执行权力,协程在进行函数调用的时候,编辑器依赖提前插入的检查代码判断该协程是否要被抢占,但如果协程是长时间执行的CPU密集型运行,协程是无法被抢占的,1.14之后,go依靠操作系统的信号机制进行强制抢占。
被动调度
go的协程在执行休眠,channel通道阻塞,网络IO阻塞,GC的时候会被动让出自己执行的权力。和主动调度不同的是,被动调度不会把G放入GMP的全局运行队列,因为当前G的状态是Gwaiting,而不是可运行状态。
抢占调度
go语言初始化的时候就会启动一个特殊的线程执行系统监控,系统监控每隔10ms会检查是否有准备就绪的网络协程,将其放到全局运行队列,同时还会判断当前协程是否执行时间过程过长,或者处于系统调用,如果是则会抢占当前的协程。
调度过程
- runtime 创建最初的线程 m0 和 Goroutine g0,并把 2 者关联。
- 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCS 个 P 构成的 P 列表。
- 例如待运行的 main 函数是 main.main,runtime 中也有 1 个 main 函数 ——runtime.main,代码经过编译后,runtime.main 会调用 main.main,程序启动时会为 runtime.main 创建 Goroutine,称它为 main Goroutine 吧,然后把 main Goroutine 加入到 P 的本地队列。
- 启动 m0,m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine。
- G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境
- M 运行 G
- G 退出,再次回到 M 获取可运行的 G,这样重复下去,直到 main.main 退出,runtime.main 执行 Defer 和 Panic 处理,或调用 runtime.exit 退出程序。