# GMP模型简述
go runtime 是 golang 并发的核心。go routine 比线程更加轻量,调度发生在用户态,且上下文切换仅需要保存几个寄存器(SP、PC、LR、BP)。内存管理会根据GMP模型实现并优化,下边做一些关于GMP的简单介绍。
GMP调度模型中,G代表 go routine ,M 代表操作系统线程,P 代表的是程序运行过程中的资源。G 保存着程序运行的状态,比如运行到了哪里,M 则是执行程序的实体,P 为 M 执行程序提供了资源,比如 P 中包含了内存分配相关的部分。当 M运行的时候,需要绑定一个 P ,当 M 进入系统调用的时候,会释放 P,这个时候 P 就可以被另外的 M 绑定,当 M 从系统调用返回,需要再绑定一个 P 才能继续执行。P 的数量不需要过多,因为能并行的线程数取决于硬件,CPU 的数量还有核心的数量。M 的数量可以超过硬件支持的最大并行线程数量,因为 总有些 M 会被系统调用阻塞,当其阻塞的时候让出P 可以供另外一个 M 继续运行。
go routine 的调度是抢占式与协作式并存的,抢占式调度实现的原理是给线程注册信号处理函数,当需要抢占一个go routine时,就往其所绑定的线程M上发送一个信号,该信号的处理函数中包含抢占的逻辑,不过抢占式调度并不是可以在任意位置中断go routine 的运行,只有当go routine处于安全点(safe-point)才可以被抢占成功。当go routine 正执行于分配内存的代码时,就不处于 safe-point ,从垃圾回收的角度看,当前go routine 处于safe-point 的前提是该go routine 当前的PC 有对应的垃圾回收相关的元数据。协作式调度实现的方式借助了栈扩增的机制,每个go routine 的初始栈大小只有2k(linux amd64),为了防止栈溢出,在每个函数执行前,都会先判断栈空间使用情况(这些逻辑都是编译过程中插入的),并以此作为是否执行栈扩增的依据,栈扩增的逻辑中就会处理 “需要被抢占的标记” ,当发现有这个标记的时候,go routine 就会主动让出 M 。
golang采用的垃圾回收算法支持在垃圾回收的过程中,程序不中断运行,但只是宏观上的,在微观的某些时间点,必须通过停止所有运行用户程序的 go routine来保证垃圾回收的准确性。所以调度的方式以及实现会影响到垃圾回收的停顿时间。
# g0
每个M 都有一个g0,g0也是 go routine ,g0 和 M 上运行的其他 go routine 的区别类似于操作系统中的内核栈和用户栈。go routine只是保存一个运行的状态,包括 PC,也包括栈。当 M 切换到运行g0的时候,类似于在操作系统中进入了内核态。当一个普通的 go routine 需要执行一些逻辑, 并且希望运行过程中不被抢占的话,可以切换到g0去执行这段逻辑,包括如果想执行一段不能在执行过程中使得栈增长的逻辑,也可以切换到g0执行(g0的栈比较大,普通go routine的栈一开始比较小,在运行过程中如果不够的话会扩增)
# 抢占式调度
在Go1.10之前(包含1.10),go routine之间的调度采用的是协作式调度。
协作式调度是协程主动让出CPU的,在函数调用开始前检测协程栈是否满足函数运行需求时的同时也会检测抢占标志,如果有抢占标志,则主动让出CPU。
协作式调度的问题是,有一些情况下可能会导致无法调度,比如下边这段代码
let count = 0
go func(){
for {
count +=1
}
}()
2
3
4
5
6
并且,协作式调度对GC的表现有很大的影响,会增加STW的时间,GC有一些环节需要所有的(至少是用户级别的协程,更严谨需要看源码)协程停止才行(比如打开关闭写屏障),所以如果暂停所有的协程就需要等每一个协程主动退出。
抢占式调度可以解决部分协程无法主动让出CPU的问题,并且一定程度上可以减少STW的时间(严谨的来说或许不是一定的,局部来看,如果某个协程不处于safe-point,就没法被抢占,所以也需要不断的重试)
抢占式调度有好几种实现的思路,比如在一些流图的基本块的后边插入抢占标记的检测。不过官方使用的是信号的方式,通过给被抢占协程运行在的线程发信号。

为什么选择使用SIGURG这个信号,主要原因是因为这个信号不常用,其次在一些libc的库里没有被使用。
抢占式调度可以发生在任意处于safe-point的指令,编译阶段需要为每条可抢占的指令生成对应的元数据信息,用于实现更精准的垃圾回收(后边会介绍这些元数据信息的压缩存储方式)
虚拟地址空间管理 →