1、Golang调度器由来
-
单进程时代的问题
- 单一执行流程,计算机只能一个一个的处理任务
- 进程阻塞带来CPU时间的浪费
-
多进程、多线程的问题
- 设计变得复杂
- 进程/线程数量越多,切换成本就越大
- 多线程随着同步竞争(锁,竞争资源冲突等)
- 设计变得复杂
-
多线程/进程的壁垒
- 高内存占用
- 进程占用内存:虚拟内存4G(32bit OS)
- 线程占用内存:约为4MB
- 高CPU调度消耗
- 高内存占用
-
- 协程(co-routine)引发的问题
- N:1
- 无法利用多个CPU
- 出现阻塞的瓶颈
- 1:1
- 切换线程/进程代价昂贵
- N:M
- 能够利用多核
- 过于依赖协程的优化和算法
- N:1
- 协程(co-routine)引发的问题
-
调度器的优化
- Goroutine优化
- 内存占用 几KB 可以大量开辟
- 灵活调度 切换成本低
- 早期GO调度器
- 基本的全局GO队列和传统的轮询利用多个thread去调度
- 弊端
- 创建、销毁、调度G都需要每个M获取锁,形成激烈的锁竞争
- M转移G会造成延迟和额外的系统负载
- 系统调用导致频繁的线程阻塞和取消阻塞操作增加了系统的开销
- Goroutine优化
2、Goroutine调度器GPM模型的设计思想
1、GMP模型简介
- GMP
- G:goroutine 协程
- M:thread 线程
- P:processor 处理器
- 全局队列
- 存放等待运行的G
- P的本地队列
- 存放等待运行的G
- 数量限制 不超过256个G
- 优先将新创建的G放在P本地队列,若满了就会放到全局队列
- P列表
- 程序启动时创建
- 最多有GOMAXPROCS个(可配置)
- M列表
- 当前操作系统分配到当前GO程序的内核线程数
- P和M的数量
- P数量问题
- 环境变量$GOMAXPROCS
- 在程序中使用runtime.GOMAXPROCS()设置
- M数量问题
- GO语言本身限制M最大数量为10000
- runtime/debug包中的setMaxThreads()设置
- 一个M阻塞会创建一个新的M
- 若有M空闲,就会回收或者睡眠
- P数量问题
2、调度器的设计策略
- 复用线程
- 避免频繁的创建、销毁线程,而是对线程的复用
- work stealing机制
- 当本线程没有可运行的G时,尝试从其他线程绑定的P中偷取G,而不是销毁线程
- hand off 机制
- 当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行
- work stealing机制
- 避免频繁的创建、销毁线程,而是对线程的复用
- 利用并行
- GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行
- 抢占
- 在coroutine中要等待一个协程主动让出CPU才能执行下一个线程,GO中,一个goroutine最多占用10ms CPU,防止其他G饿死
- 全局G队列
- 当M执行work stealing从其他P偷不到G时,可以从全局G队列获取G
3、"go func()"执行过程
- 通过go func()创建一个G
- 有两个存储G的队列,,一个是局部调度器的本地队列P,一个是全局G队列,新创建的G会先保存在本地队列P,若P满了就会保存到全局G队列
- G只能运行在M中,一个M必须持有一个P,M会从P的本地队列弹出一个可以执行的G执行,若P的本地队列为空,就会从全局G队列或其他MP组合偷取一个可执行的G来执行
- 一个M调度G执行的过程是一个循环机制
- 当M执行某一个G时若发生了,syscall或者其他阻塞操作,M会阻塞,若当前有一些G在执行,runtime就会把这个线程M从P中摘除(detach),然后创建一个新的线程(若有空闲线程则复用)服务于这个P
- 、当M系统调用结束时,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列,若获取不到P,次线程M就会进入休眠状态,加入到空闲线程中,然后这个G会被放入全局队列
4、调度器生命周期
- M0
- 启动程序之后编号为0的主线程
- 在全局变量runtime.m0中,不需要在heap上分配
- 负责初始化操作和启动第一个G
- 启动第一个G之后,M0和其他M就一样了
- G0
- 每次启动一个M,都会第一个创建的goroutine,这就是G0
- G0仅用于负责调度的G
- G0不指向任何可执行函数
- 每个M都会有一个自己的G0
- 在调度或系统调用时会使用G0的栈空间
- 全局变量的G0是M0的G0
5、可视化GMP编程
- 基本的trace编程
- 创建trace文件
- f, err := os.Create(“trace.out”)
- 开启trace
- trace.Start(f)
- 关闭trace
- teace.Stop(f)
- go build且运行后会得到trace.out文件
- 创建trace文件
- 通过go tool trace工具打开trace文件
- 通过debug trace查看GMP信息,GODEBUG=schedtrace=1000 ./可执行程序
- SCHED 调试的信息
- 0ms从程序启动到输出的时间
- gomaxprocs P的数量 默认和核心数一致
- idleprocs处理idle状态的P的数量 gomaxprocs-idleprocs=当前正在执行P的数量
- threads 全部线程数
- spinningthreads 处于自旋状态的线程
- idlethread处于idle状态的thread
- runqueue 全局G队列中G的数量
- [0,0] 每个P的本地队列中,目前存在G的数量