GO 协程调度器原理与GMP设计思想
Golang 调度器的由来
Goroutine GMP模型的设计思想
GMP调度场景的全过程分析
掌握golang协程调度器原理 为什么golang协程的调度是很快的
调度器的由来
单进程时代的问题:单一执行流程,计算机只能一个一个任务进行处理;进程阻塞会带来cpu 的浪费
多进程多线程的问题:
设计变得复杂: 进程线程数量越多,切换成本越大,造成cpu性能的浪费
线程伴随同步竞争 锁,竞争资源冲突
多进程多线程的壁垒:进程线程占用内存较高
高cpu调度的消耗
协程:
N:1
1:1
M:N : 依赖调度器的优化
go调度器的优化:Goroutine:小的内存占用(j几KB),可以大量开辟
灵活调度,切换成本较低
早起go 的调度器:基本的全局GOroutine队列和比较传统的轮询利用多个thread去调度(需要获取锁)
并发调度GPM
P processor 作用类似CPU核,用来控制并发执行的任务数量,可通过runtime.GOMAXPROCS执行并发数量,默认为CPU核心数量。每个工作线程M 都必须绑定一个有效的P 才被允许执行任务,否则只能休眠,直到有空闲P 时被唤醒。P维护一个本地队列存放待执行的G, 工作线程 M 独享所绑定的P 资源, 可在无锁状态下进行高效操作
G goroutine 一个进程的一切都在以goroutine 的方式运行,包括main函数。G并非执行实体,仅保存相关任务状态和提供任务执行所需要的栈空间(初始大小2KB)。使用 go 关键字创建并发goroutine 任务,任务创建后被保存在P 的本地队列或全局队列,等待被工作线程调度执行。
M machine 系统线程,工作时与P 绑定,以循环的方式执行G 并发任务,M 通过修改寄存器的值, 将工作栈指向当前所执行的G 的栈空间开始执行任务函数。需要中断执行时, 保存寄存器的值到栈内, 任意M 都可以以此恢复执行。线程只负责执行工作,不负责保存状态。
P/M 构成组合体,M的数量多于P 的数量,当M 因陷入系统调用而长时间阻塞时,P 会被收回,新建或唤醒空闲线程执行,造成M 的数量增加。
GMP模型 G:goroutine M:thread P:processer处理器
全局队列:存放等待的G
P的本地队列:存放等待的G,数量限制不超过256G,优先将新创建的G放在P的本地队列中,如果满了就放在全局队列中
P列表: 程序启动时创建,最多有GOMAXPROCES个(可配置)
M列表:当前操作系统分配到当前Go程序的内核线程数,最大量是10000个。有一个M阻塞(空闲)会创建(回收)一个。
GMP调度器的设计策略
复用线程:work stealing(本地无可运行G,可尝试从其他本地队列中偷取G),head off(当线程M运行G时发生阻塞,M会将P转交给M0,自己继续关注G)
利用并行:可通过设置GOMAXPROCES设置P的数量,最多有GOMAXPROCES个线程分布在多个CPU上同时运行
抢占:一个goroutine最多占用时间片10ms,防止其他goroutine被饿死
全局G队列:当work stealing从其他的本地队列中偷不到goroutine会从全局队列中获取G。 需要加锁,效率较低
“go func” 的过程
go func创建一个goroutine (G)加入当前M的本地队列,如果本地队列已满,就会加入全局队列。
G必须运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列中弹出一个可执行状态的G来执行,如果P的本地队列为空,则会从其他P的队列或全局队列中偷取一个。
当M在执行G时发生syscall或者其他的操作导致阻塞,runtime会将这个线程M从P中摘除,并创建一个新的操作系统线程来服务于这个P(或空闲的线程)
当阻塞结束后,G会常去获取一个空闲的P继续执行,放入P 的本地队列中,如果获取不到,则放入全局队列中,此时M进入休眠状态
调度器的生命周期
M0:启动程序后编号为0的主线程;在整个进程中唯一不需要再heap上分配;负责执行初始化操作和启动第一个G;
G0:每次启动一个M,都会第一个创建的goroutine,就是G0;G0仅用于调度,不指向任何可执行的函数;每个M都有一个自己的G0;M0的G0在全局空间
go tool trace 工具
GODEBUG=schedtracd=1000 ./xxx
GMP调度器场景分析
场景1
P拥有G1,M1获取G1后开始运行G1,G1使用go gunc 创建了G2,为了保证局部性,G2会加入到当前P的本地队列中
场景2
G1执行完毕,,M会先切换为自己的G0,通过G0进行调度,优先从自己本地队列的获取G,再考虑偷取
场景3
G2创建了多个G,本地队列已满,会先将本地队列中前一半的G打乱后和新创建的G共同放入全局队列(不太明白原因)
场景4
自旋线程(运行G0,没有其他G并不断寻找G)
场景5
自旋线程 从全局队列到本地队列的负载均衡
从全局队列中获取G的个数
场景6
自旋线程 从其他本地队列中偷取G
场景7
G发生调用阻塞后,M与P接触绑定,如果P的本地队列中G或者全局队列中有G 并且存在空闲的M,P寻找新的M与之绑定
M与P解绑,但是M会记住P,在G结束系统调用阻塞状态后,尝试获取P,如果无法获取,则获取空闲的其他P,依然没有的话会将G放入全局队列,M进入休眠