整理自:Golang深入理解GPM模型
总结
掌握Golang协程调度器的原理,为什么Go的协程调度那么快?
Go的调度器做了很多事情来避免过多的操作系统线程抢占,通过窃取调度(stealing机制)它们到正确的和未充分利用的处理器,以及实现自旋线程以避免过高阻塞或者解除阻塞切换的发生。
一、Golang “调度器”的由来
1.单进程时代的问题
1). 单一执行流程,计算机只能一个任务一个任务的执行;
2).进程阻塞所带来的CPU浪费时间;
2.多进程、多线程的问题
1).设计变得复杂;进程/线程数量越多,切换成本越大,也就更浪费;
2).同步竞争,如锁、竞争资源等冲突;
3.多进程、多线程的壁垒:高内存占用,高CPU调度切换;
4.协程(co-routine)引发的问题
N:1 : 无法利用多个CPU,出现阻塞瓶颈;
1:1 :和多线程、多进程无区别,切换协程成本代价昂贵;
M:N :能够利用多核,但过度依赖协程调度器的优化和算法;
二、Groutine调度器的设计思想
GMP模型简介
G:Groutine 协程, go程序建立的用户线程。
M:machine内核线程,每个M都有一个线程的栈。
P:processor 处理器
全局队列
存放等待运行的groutine
P的本地队列
存放等待运行的G;数量限制,不超过256G;优先将创建的G放在P的本地队列中,满了之后才会放入全局队列;
P列表
程序启动时创建;最多有GOMAXPROCS个(可配置);
M列表
当前操作系统分配到当前Go程序的内核线程数;有一个M阻塞,会创建或者唤醒一个新的M,若有空闲,则会回收或睡眠此M;
调度器的设计策略
复用线程
避免频繁的创建、销毁线程,而是对线程的复用;
Work stealing机制
当本线程的P队列中没有G可用并且全局队列为空时,会主动去其他线程的P队列中获取,避免线程的销毁;
handle off 机制
当本线程的G运行阻塞时,会释放绑定的P队列,把P队列转移到其他空闲的线程上去;
利用并行
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在CPU上运行;
抢占
一个Goroutine 最多占用CPU 10ms 就会交给其他 协程,防止其被饿死;
全局G队列
当M执行work stealing 机制时,无法从其他P中获取G,就会从全局队列中获取;
调度器的生命周期
M0 :
启动程序后编号为0
的主线程,负责执行初始化操作和启动第一个G,启动第一个G后,M0就和其他M一样了
G0:
每次启动一个M,都会创建一个groutine,这就是G0;G0仅用于调度其他的G;G0不指向任何可执行的函数,每个M都会有自己的G0;在调度和系统调用时会使用到G0的栈空间来调度;
M0和G0会放在全局空间;全局变量的G0是M0的G0,一般M的G0会放在自己的本地队列中;
可视化的GMP编程
基本的trace编程
1.创建trace文件 c, err := os.Create("trace.out")
2.启动 trace.Start(c)
3.停止trace.Stop()
4.go build
运行后,会得到一个trace.out
文件
5.通过go tool trace
打开trace文件,go tool trace trace.out
通过 debug trace 查看 GMP 信息
GODEBUG=schedtrace=1000 ./可执行程序
三、Go调度器GMP调度场景的过程分析
场景一:创建G
P拥有G1,M1获取P后开始运行G1,G1使用go func()
创建了G2,为了局部性G2优先加入到P的本地队列中。
场景二:G1执行完毕
G1运行完成后(函数goexit),线程M上运行的groutine切换为G0,G0负责调度协程的切换(函数:schedule),从P的本地队列中取出G2,从G0切换到G2,并开始运行G2(函数:execute),实现了线程M的复用。
场景三、四、五:G2创建多个G
如果创建多个G,首先会把本地P队列装满,如果本地队列已满但还有未创建完的G,会将本地对列中的前一半G打乱顺序和刚创建的G一起放入全局队列中,还有未创建的G时,就将加入空出来的本地队列。
场景六:唤醒正在休眠的M
规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M的组合去执行。
假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程(没有G但是为运行状态的线程,不断寻找G)
场景七:被唤醒的M2从全局队列批量获取G
M2尝试从全局队列(简称"GQ")取一批G放到P2的本地队列(函数:findrunnable()),M2从全局队列取的G数量符合下面的公式:
n=min(len(GQ)/GOPAXPROCS+1,len(GQ/2))
解释:n 等于 最小值 ( 全局队列个数 除以 P 的个数 加 1 , 全局队列除以2的个数 )
当M2获取到G时,G0调度G运行后,将不再是自旋线程;从全局队列到P的本地队列这一过程就是GMP内部的负载均衡。
场景八:M2从M1中偷去G
当全局队列中没有G,那么 M2 就要执行 work stealing 机制,从其他有G的P队列中偷取后面的一半,放在自己的P本地队列中。
场景九:自旋线程的最大限制
任何时候最多有 GOMAXPROCS 个自旋的M,当一个自旋线程找到工作时,它就脱离了自旋。
如果有空闲的M没有绑定P,那么被绑定P的空闲线程不会被阻塞,当新的G被创建或阻塞时,调度器确保至少有一个自旋M,这保证了没有可运行的G没被运行,并且避免过多的M阻塞或解除阻塞。
场景十:G发生系统调用/阻塞
当G1在M1阻塞时,G1会在M1上继续等待,但会唤醒一个休眠的M2,将M1绑定的P1转移至M2,继续执行P1本地队列中的G2。
自旋线程依然自旋,自旋线程只会偷去G,而不会去偷去P,因为它本身就有自己空闲的P。
场景十一:G发生非阻塞
接场景十,当G1非阻塞时,M1会尝试去寻找空闲的P进行绑定,以继续执行G1,当找不到可供绑定的空闲P时,G1会被加入全局队列,供其他P获取,M1会进入休眠状态。