上一篇文章《Go语言高阶:调度器系列(1)起源》,学goroutine调度器以前的一些背景知识,这篇文章则是为了对调度器有个宏观的认识,从宏观的3个角度,去看待和理解调度器是什么样子的,但仍然不涉及具体的调度原理。git
三个角度分别是:github
在开始前,先回忆下调度器相关的3个缩写:golang
3者的简要关系是P拥有G,M必须和一个P关联才能运行P拥有的G。数组
《Go语言高阶:调度器系列(1)起源》中介绍了协程和线程的关系,协程须要运行在线程之上,线程由CPU进行调度。bash
在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工做线程上。less
Go的调度器也是通过了多个版本的开发才是如今这个样子的,函数
在
$GOROOT/src/runtime/proc.go
的开头注释中包含了对Scheduler的重要注释,介绍Scheduler的设计曾拒绝过3种方案以及缘由,本文再也不介绍了,但愿你不要忽略为数很少的官方介绍。
Tony Bai在《也谈goroutine调度器》中的这幅图,展现了goroutine调度器和系统调度器的关系,而不是把两者割裂开来,而且从宏观的角度展现了调度器的重要组成。测试
自顶向下是调度器的4个部分:ui
Goroutine调度器和OS调度器是经过M结合起来的,每一个M都表明了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。spa
接下来咱们从另一个宏观角度——生命周期,认识调度器。
全部的Go程序运行都会通过一个完整的调度器生命周期:从建立到结束。
即便下面这段简单的代码:
package main import "fmt" // main.main func main() { fmt.Println("Hello scheduler") }
也会经历如上图所示的过程:
main.main
,runtime
中也有1个main函数——runtime.main
,代码通过编译后,runtime.main
会调用main.main
,程序启动时会为runtime.main
建立goroutine,称它为main goroutine吧,而后把main goroutine加入到P的本地队列。main.main
退出,runtime.main
执行Defer和Panic处理,或调用runtime.exit
退出程序。调度器的生命周期几乎占满了一个Go程序的一辈子,runtime.main
的goroutine执行以前都是为调度器作准备工做,runtime.main
的goroutine运行,才是调度器的真正开始,直到runtime.main
结束而结束。
上面的两个宏观角度,都是根据文档、代码整理出来,最后咱们从可视化角度感觉下调度器,有2种方式。
方式1:go tool trace
trace记录了运行时的信息,能提供可视化的Web页面。
简单测试代码:main函数建立trace,trace会运行在单独的goroutine中,而后main打印"Hello trace"退出。
func main() { // 建立trace文件 f, err := os.Create("trace.out") if err != nil { panic(err) } defer f.Close() // 启动trace goroutine err = trace.Start(f) if err != nil { panic(err) } defer trace.Stop() // main fmt.Println("Hello trace") }
运行程序和运行trace:
➜ trace git:(master) ✗ go run trace1.go Hello trace ➜ trace git:(master) ✗ ls trace.out trace1.go ➜ trace git:(master) ✗ ➜ trace git:(master) ✗ go tool trace trace.out 2019/03/24 20:48:22 Parsing trace... 2019/03/24 20:48:22 Splitting trace... 2019/03/24 20:48:22 Opening browser. Trace viewer is listening on http://127.0.0.1:55984
效果:
从上至下分别是goroutine(G)、堆、线程(M)、Proc(P)的信息,从左到右是时间线。用鼠标点击颜色块,最下面会列出详细的信息。
咱们能够发现:
runtime.main
的goroutine是g1
,这个编号应该永远都不变的,runtime.main
是在g0
以后建立的第一个goroutine。main.main
,建立了trace goroutine g18
。g1运行在P2上,g18运行在P0上。go tool trace的资料并很少,若是感兴趣可阅读:https://making.pusher.com/go-... ,中文翻译是:https://mp.weixin.qq.com/s/nf... 。
方式2:Debug trace
示例代码:
// main.main func main() { for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println("Hello scheduler") } }
编译和运行,运行过程会打印trace:
➜ one_routine2 git:(master) ✗ go build . ➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000 ./one_routine2
结果:
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
看到这密密麻麻的文字就有点担忧,不要愁!由于每行字段都是同样的,各字段含义以下:
[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信息。
命令:
➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2
结果:
截图可能更代码匹配不起来,最初代码是for死循环,后面为了减小打印加了限制循环5次
每次分别打印了每一个P、M、G的信息,P的数量等于gomaxprocs
,M的数量等于threads
,主要看圈黄的地方:
这篇文章,从3个宏观的角度介绍了调度器,也许你依然不知道调度器的原理,内心感受模模糊糊,不要紧,一步一步走,经过这篇文章但愿你了解了:
本文全部示例代码都在Github,可经过阅读原文访问:golang_step_by_step/tree/master/scheduler
最近的感觉是:本身懂是一个层次,能写出来须要抬升一个层次,给他人讲懂又须要抬升一个层次。但愿朋友们有所收获。
- 若是这篇文章对你有帮助,请点个赞/喜欢,感谢。
- 本文做者:大彬
- 若是喜欢本文,随意转载,但请保留此原文连接:http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/