Golang对协程的处理
Golang早期调度器的处理
GMP
G | goroutine协程 |
P | processor处理器 |
M | thread线程 |
- 全局队列 :用来存放等待运行的G
- 本地队列 :
- 存放等待运行的G
- 数量限制,不超过256G
- 优先将创建的G放在P的本地队列中,如果满了会放在全局队列
- P列表
- 程序启动时创建
- 最多有GOMAXPROCS个(可配置)
- M列表
- 当前操作系统分配到当前Go程序的内核线程数
- P和M的数量
- P的数量问题
- 设置环境变量$GOMAXPROCS
- 在程序中通过runtime.GOMAXPROCS()来设置
- M的数量问题
- GO本身限定M的最大是10000
- runtime/debug包中的SetMaxThreads函数来设置
- 有一个M阻塞,会创建一个新的M
- 如果有M空闲,那么会回收或者睡眠
调度器设计策略
复用线程
work stealing机制
当本地线程M2无可运行的G时,尝试从其他线程绑定的P中偷取G,而不是销毁线程M2
hand off机制
本地线程M1因G1进行系统调用阻塞时,线程释绑定的P,把P转移给其他的空闲线程执行,在运行阻塞G1的M1中的G1重新运行后,如果G1还在运行,那么M1继续执行,如果G1运行完成,那么M1随即进入睡眠或被销毁
利用并行
GOMAXPROCS限定P的个数=CPU核数/2
设置P的个数,最多有GOMAXPROCS个线程分布在多个CPU上同时运行
抢占
在coroutine中要等待一个协程主动让出CPU才执行下一个协程
在Go中,一个goroutine最多占用CPU10ms,防止其他goroutine被饿死
全局G队列
work stealing机制,从全局偷取
当M2执行work stealing从其他P偷不到G时,他会从全局G队列中获取G,这个获取的过程时需要加锁的
GO指令调度流程
- 通过Go func() 创建一个goroutine
- 有两个存储G的队列,一个是局部的调度器P的本地队列、一个是全局G队列。新创建的G会优先保存在本地队列中,如果本地队列满了,就会存放在全局队列中。
- G只能运行在M中,一个M必须持有一个P,M与P的关系是1:1。M会从本P的本地队列中弹出一个可执行状态的G来执行,如果P的本地队列为空,那么就会从其他的组合中偷取一个可执行的G来执行,又或者去全局队列中获取一个可执行的G
- 假设M在执行G的时候,G发生了systemcall或者其他阻塞操作,M会阻塞。如果当前有一些G正在执行,runtime会把这个线程M从P中摘除,然后创建一个新的M(如果有空闲的线程就会复用空闲的线程)来服务于摘除的P。这个时候被摘除的M就和阻塞的G绑定在一起了,在执行完G之后,M可能会被休眠或销毁。又或者重新接管P(P没有被其他的M给绑定)
调度器生命周期
- M0
- 启动程序后编号为0的主线程
- 在全局runtime.m0中,不需要在heap上分配
- 负责执行初始化操作和启动第一个G
- 启动第一个G后,M0和其他的M一样
- G0
- 每次启动一个M,都会第一个创建的gorutine,就是G0
- G0仅用于负责调度G
- G0不指向任何可执行的函数
- 每个M都会有一个自己的G0
- 在调度或系统调用时会使用M切换到G0,来调度M0的G0会放在全局空间
GMP可视化编程
- 创建trace文件 os.Create("trace.out")
- 启动trace trace.Start(f)
- 停止trace trace.Stop()
- go build 并且运行后可以得到一个trace.out文件
- 通过go tool trace 打开tarce文件 go tool trace trace.out
打开View trace
- Gotoutines G协程信息
- Goroutines Value G0
- Goroutines Value G1
- Heap 堆栈信息
- Threads M线程信息
- Threads Value 0 M0
- PROCS P调度器信息
通过Debug trace查看GMP信息
GODEBUG=schedtrace=1000 ./可执行程序
- SCHED 调试的信息
- 0ms 从程序启动到输出的时间
- gomaxprocs P的数量 一般默认是和CPU的核心数是一致的
- idleprocs 处理idle状态P的数量,gomaxprocs - idleprocs= 目前正在执行的P的数量
- threads 线程数量(包括M0,也包括当前GODEBUG调试线程)
- Spinningthreads 处于自旋状态thread的数量
- Idlethread 处理idle状态thread
- Runqueue 全局G队列中的G的数量
- [0 0 0 0 0 0 0 0] 每个P的local queue本地队列中,目前存在的G的数量
GMP调度分析
G1创建G3
P拥有G1,M1获取P后开始运行G1,G1使用go func()创建了G3,为了局部性G3优先加入到P的本地队列中
G1执行完毕
G1运行完成之后,M上运行的Goroutine切换为G0,G0负责调度时协程切换(schedule)从P本地队列取出G2,从G0切换到G2,并且开始运行G2,实现线程M的复用
开辟过多的G
- 如果连续开辟过多的G,会先把本地G存满
- 如果本地已经满了,还需要继续创建,那么就会把本地队列前一半的G打乱顺序,和新创建的G放到全局队列中
- 继续创建G,这个时候本地队列没满,那么就会新创建的G存放在本地队列
唤醒正在休眠的M
在创建G时,运行的G3会尝试唤醒其他空闲的P和M组合去执行,假设G3唤醒了M1,M1绑定了P1,首先要运行G0,但是由于P1本地队列中不存在G。那么M2此时就会进入自旋状态,变成自旋线程(没有G但是为运行状态的线程,不断寻找G)
被唤醒的M从全局队列中取批量的G
从全局队列中取P的过程称之为负载均衡
偷取G
假设全局队列中已经没有G了,那么M1会进行work stealing,会从其他有G的P中偷取一半的G,放在自己的本地P队列中,然后由自身的G0进行调度,并且运行G。图中M绑定的P一半剩下后只有G9被偷取
自旋线程的最大限制
自旋线程+执行线程 <= GOMAXPROCS
G发生系统调用/阻塞
当G4发生了系统调用或者阻塞的时候,会尝试从休眠线程队列中唤醒一个M,去和P1绑定,并且让唤醒的M去执行G5,自旋线程只存在偷取G的情况,所以不会去绑定其他的P,因为自身已经绑定了P
G系统调用结束/非阻塞
加入G4完成了系统调用或者非阻塞了,与之绑定的M会去寻在之前的P1,查看是否可以绑定,加入不能绑定,那么回去空闲P队列中查看,如果空闲P队列中也没有P,那么G4会被放回全局队列等待调度