⼀、Golang“调度器”由来
协程(co-routine), 引发的问题?(M:线程 N:协程)
N:1 1.⽆法利⽤多个CPU 2.出现阻塞的瓶颈
1:1 1.跟多线程/多进程模型⽆异 2.切换协程成本代价昂贵
M:N 1.能够利⽤多核 2.过于依赖协程调度器的优化和算 法
go-routine的优化?
内存占⽤ ⼏KB ⼤量开辟
灵活调度 切换成本低
早期的Go的调度器 基本的全局Go队列和⽐较传统的 轮询利⽤多个thread去调度
弊端
1. 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
2. M转移G会造成延迟和额外的系统负载。
3. 系统调⽤(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
⼆、Goroutine调度器的GMP模型的设计思想
GMP模型简介:
G:goroutine 协程
M:thread 内核线程
P:processor 处理器
全局队列 存放等待运⾏的G
P的本地队列: 1存放等待运⾏的G 2数量限制 不超过256G 3 优先将新创建的G放在P的本地队 列中,如果满了会放在全局队列 中
P列表: 1程序启动时创建 2最多有GOMAXPROCS个(可配置)
M列表: 当前操作系统分配到当前Go程序的内核线程数
P的数量: 1.环境变量$GOMAXPROCS 2.在程序中通过runtime.GOMAXPROCS() 来设置
M的数量问题: 1.Go语⾔本身 是限定M的最⼤量是10000(忽略) 2.runtime/debug包中的SetMaxThreads函数来设置 3.有⼀个M阻塞,会创建⼀个新的M 4.如果有M空闲,那么就会回收或者睡眠
调度器的设计策略
1.复⽤线程 避免频繁的创建、销毁线程,⽽是对线程的复⽤。
(1)work stealing机制 当本线程⽆可运⾏的G时,尝试 从其他线程绑定的P偷取G,⽽不 是销毁线程。
(2)hand off机制 当本线程因为G进⾏系统调⽤阻 塞时,线程释放绑定的P,把P转 移给其他空闲的线程执⾏。
2.利⽤并⾏ GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运⾏。
3.抢占 在coroutine中要等待⼀个协程主动让出CPU才执⾏下⼀个协程,在Go 中,⼀个goroutine最多占⽤CPU 10ms,防⽌其他goroutine被饿死
4.全局G队列 当M执⾏work stealing从其他P偷不到G时,它可以从全局G队列获取G。
三.“go func()” 经历了什么过程
1、我们通过 go func()来创建⼀个goroutine;
2、有两个存储G的队列,⼀个是局部调度器P的本地队列、⼀个是全局G队列。新创建的G会先 保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;
3、G只能运⾏在M中,⼀个M必须持有⼀个P,M与P是1:1的关系。M会从P的本地队列弹出⼀个可执 ⾏状态的G来执⾏,如果P的本地队列为空,就会想其他的MP组合偷取⼀个可执⾏的G来执⾏;
4、⼀个M调度G执⾏的过程是⼀个循环机制;
5、当M执⾏某⼀个G时候如果发⽣了syscall或则其余阻塞操作,M会阻塞,如果当前有⼀些G在执⾏, runtime会把这个线程M从P中摘除(detach),然后再创建⼀个新的操作系统的线程(如果有空闲的线程可 ⽤就复⽤空闲线程)来服务于这个P;
6、当M系统调⽤结束时候,这个G会尝试获取⼀个空闲的P执⾏,并放⼊到这个P的本地队列。如果获取不 到P,那么这个线程M变成休眠状态, 加⼊到空闲线程中,然后这个G会被放⼊全局队列中。
四.调度器的⽣命周期
M0 M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap 上分配,M0负责执⾏初始化操作和启动第⼀个G, 在之后M0就和其他的M⼀样了。
G0 G0是每次启动⼀个M都会第⼀个创建的gourtine,G0仅⽤于负责调度的G,G0不指向任何可执⾏的函数, 每个M都会有⼀个⾃⼰的G0。在调度或系统调⽤时会使⽤G0的栈空间, 全局变量的G0是M0的G0。
五.可视化的GMP编程
创建trace⽂件 f, err := os.Create("trace.out")
启动trace trace.Start(f)
停⽌trace trace.Stop()
go build 并且运⾏之后,会得到⼀个trace.out⽂件
go tool trace trace.out
通过http://127.0.0.1:33479进⾏访问
通过Debug trace查看GMP信息