上一篇文章
《Go语言高阶:调度器系列(1)起源》
,学goroutine调度器之前的一些背景知识,
这篇文章则是为了对调度器有个宏观的认识,从宏观的3个角度,去看待和理解调度器是什么样子的,但仍然不涉及具体的调度原理
。
三个角度分别是:
调度器的宏观组成
调度器的生命周期
GMP的可视化感受
在开始前,先回忆下调度器相关的3个缩写:
G
: goroutine,每个G都代表1个goroutine
M
: 工作线程,是Go语言定义出来在用户层面描述系统线程的对象 ,每个M代表一个系统线程
P
: 处理器,它包含了运行Go代码的资源。
3者的简要关系是P拥有G,M必须和一个P关联才能运行P拥有的G。
《Go语言高阶:调度器系列(1)起源》
中介绍了协程和线程的关系,协程需要运行在线程之上,线程由CPU进行调度。
在Go中,
线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上
。
Go的调度器也是经过了多个版本的开发才是现在这个样子的,
1.0版本发布了最初的、最简单的调度器,是G-M模型,存在4类问题
1.1版本重新设计,修改为G-P-M模型,奠定当前调度器基本模样
1.2版本
加入了抢占式调度,防止协程不让出CPU导致其他G饿死
在
$GOROOT/src/runtime/proc.go
的开头注释中包含了对Scheduler的重要注释,介绍Scheduler的设计曾拒绝过3种方案以及原因,本文不再介绍了,希望你不要忽略为数不多的官方介绍。
Tony Bai
在
《也谈goroutine调度器》
中的这幅图,展示了goroutine调度器和系统调度器的关系,而不是把二者割裂开来,并且从宏观的角度展示了调度器的重要组成。
自顶向下是调度器的4个部分:
全局队列
(Global Queue):存放等待运行的G。
P的本地队列
:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G’时,G’优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
P列表
:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS个。
M
:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列
拿
一批G放到P的本地队列,或从其他P的本地队列
偷
一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行
。
https://making.pusher.com/go-tool-trace/
,中文翻译是:
https://mp.weixin.qq.com/s/nf_-AH_LeBN3913Pt6CzQQ
。
方式2:Debug trace
示例代码:
1 2 3 4 5 6 7
|
func main() { for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println("Hello scheduler") } }
|
编译和运行,运行过程会打印trace:
1 2
|
➜ one_routine2 git:(master) ✗ go build . ➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000 ./one_routine2
|
1 2 3 4 5 6 7 8 9 10 11
|
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 1001ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] Hello scheduler SCHED 2002ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] Hello scheduler SCHED 3004ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] Hello scheduler SCHED 4005ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] Hello scheduler SCHED 5013ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] Hello scheduler
|
看到这密密麻麻的文字就有点担心,不要愁!因为每行字段都是一样的,各字段含义如下:
SCHED:调试信息输出标志字符串,代表本行是goroutine调度器的输出;
0ms:即从程序启动到输出这行日志的时间;
gomaxprocs: P的数量,本例有8个P;
idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;
threads: os threads/M的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;
spinningthreads: 处于自旋状态的os thread数量;
idlethread: 处于idle状态的os thread的数量;
runqueue=0: Scheduler全局队列中G的数量;
[0 0 0 0 0 0 0 0]
: 分别为8个P的local queue中的G的数量。
看第一行,含义是:刚启动时创建了8个P,其中5个空闲的P,共创建5个M,其中1个M处于自旋,没有M处于空闲,8个P的本地队列都没有G。
再看个复杂版本的,加上
scheddetail=1
可以打印更详细的trace信息。
1
|
➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2
|
截图可能更代码匹配不起来,最初代码是for死循环,后面为了减少打印加了限制循环5次
每次分别打印了每个P、M、G的信息,P的数量等于
gomaxprocs
,M的数量等于
threads
,主要看圈黄的地方:
第1处:P1和M2进行了绑定。
第2处:M2和P1进行了绑定,但M2上没有运行的G。
第3处:代码中使用fmt进行打印,会进行系统调用,P1系统调用的次数很多,说明我们的用例函数基本在P1上运行。
第4处和第5处:M0上运行了G1,G1的状态为3(系统调用),G进行系统调用时,M会和P解绑,但M会记住之前的P,所以M0仍然记绑定了P1,而P1称未绑定M。
golang_step_by_step/tree/master/scheduler
Go程序的“一生”
也谈goroutine调度器
Debug trace, 当前调度器设计人Dmitry Vyukov的文章
Go tool trace中文翻译
Dave关于GODEBUG的介绍
最近的感受是:自己懂是一个层次,能写出来需要抬升一个层次,给他人讲懂又需要抬升一个层次。希望朋友们有所收获。
如果这篇文章对你有帮助,不妨关注下我的Github,有文章会收到通知。
本文作者:
大彬
如果喜欢本文,随意转载,但请保留此原文链接:
http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/
关注公众号,获取最新Golang文章