GO 调度机制
调度器初始化
- 初始化G0栈
MOVQ $runtime·g0(SB), DI - m0和g0相互绑定
// save m->g0 = g0 MOVQ CX, m_g0(AX)
// save m0 to g0->m MOVQ AX, g_m(CX) - 暂存命令行参数
CALL runtime·args(SB) - 获取cpu核心数
CALL runtime·osinit(SB) - 初始化调度函数,会初始化P,P0绑定M0
CALL runtime·schedinit(SB) - 创建主G,执行runtime.main
CALL runtime·newproc(SB) - 开启调度循环,里面会调用schedule函数 CALL runtime·mstart(SB)
- runtime.main函数启动监控线程、初始化runtime包。开启gc,调用main.main
调度器初始化过程
- 初始化g0栈空间,绑定g0和m0
- 初始化P,P0绑定M0
- 创建主goroutine,执行runtime.main函数
- runtime.main函数启动监控线程,初始化runtime包、开启gc ,初始化main包,执行main.main函数
调度器的目标
将G调度到内核线程上
go scheduler核心思想
- 1)重用线程。
- 2)限制同时运行(不包含阻塞)的线程数为 N,N 等于 CPU 的核心数目。
- 3)线程私有 runqueues,并且可以从其他线程偷取 goroutine 来运行,线程阻塞后,可以将 runqueues 传递给其他线程
- 4)Go scheduler 会启动一个后台线程 sysmon,用来检测长时间(超过 10 ms)运行的 goroutine,将其“停靠”到 global runqueues。这是一个全局的 runqueue,优先 级比较低,以示惩罚
G0的作用
每个新的goroutine都是当前goroutine切换到G0栈上创建的
调度循环
调度循环指从调度协程g0开始,找到接下来将要运行的协程g 从协程g切换到协程g0开始新一轮调度的过程。
调度时机
- 主动调度:这主要是通过用户在代 码中执行runtime.Gosched函数实现的。主动调度的原理比较简单,需要先从当前协程切换到协程g0,取 消G与M之间的绑定关系,将G放入全局运行队列,并调用schedule函数开始新一轮的循环。
- 被动调度:被动调度指协程在休眠、channel通道堵塞、网络I/O堵塞、执行 垃圾回收而暂停时,被动让渡自己执行权利的过程。
- 抢占调度:为了让每个协程都有执行的机会,并且最大化利用CPU资源,Go语 言在初始化时会启动一个特殊的线程来执行系统监控任务。系统监控 在一个独立的M上运行,不用绑定逻辑处理器P,系统监控每隔10ms会 检测是否有准备就绪的网络协程,并放置到全局队列中。和抢占调度 相关的是,系统监控服务会判断当前协程是否运行时间过长,或者处 于系统调用阶段,如果是,则会抢占当前G的执行
抢占调度
抢占就是把当前运行的G,放弃M,放入到全局队列中 1.14版本之前,基于函数调用,1.14后基于信号量
M:N模型
就是M个G在N个线程上执行
work stealing
当一个 P 发现自己的 LRQ 已经没有 G 时,会从其他 P “偷” 一些 G 来运行,自己的工作做完了,为了全局的利益,主动为别人分担。这被称为工作窃取(Work-stealing),Go 从 1.1 开 始支持这种模式。当 P2 上的一个 G 执行结束,它就会去 LRQ 获取下一个 G 来执行。如果 LRQ 已经空了,就是说本地可运行队列已经没有 G 需要执行,并且这时 GRQ 也没有 G 了。这时,P2 会随机选择一个 P (假设为 P1),P2 会从 P1 的 LRQ “偷”过来一半的 G
G状态流转图
1、Gidle 2、Grunnable 3、running 4、syscall 5、waiting 6、dead 7、copystack 8、preempted
newproc函数
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func () {
newg := newproc1(fn, gp, pc, false, waitReasonZero) // 创建goroutine
pp := getg().m.p.ptr()
runqput(pp, newg, true) //放入p本地队列,如果本地队列满了,则放入全局队列中,第三个参数true,表示优先级高
if mainStarted {
wakep()
}
})
}
schedule函数
- 通过findrunnable函数找到一个可以运行的G,然后切换到这个G上,调用execute函数执行G,找不到G则休眠
findrunnable函数
先从本地Prunq中获取,如果获取不到,通过加锁方式从全局队列里获取,每60次,从全局队列中获取一个G,全局队列里没有获取到,执行netpoll函数,就绪列表不为空,再取窃取
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp := globrunqget(pp, 1)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
if gp, inheritTime := runqget(pp); gp != nil {
return gp, inheritTime, false
}
// global runq
if sched.runqsize != 0 {
lock(&sched.lock)
gp := globrunqget(pp, 0)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
//窃取G
gp, inheritTime, tnow, w, newWork := stealWork(now)
G退出过程
对于主 goroutine,在执行完用户定义的 main 函数的所有代码后,直接调用 exit(0) 退出整个 进程。 对于普通 goroutine 则需要经历一系列的过程:先是跳转到提前设置好的 goexit 函数的第二 条指令,然后调用 runtime.goexit1,接着调用 mcall(goexit0),而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。
netpoll
netpoll是指 Go runtime 中借助于epoll对套接字进行批量监听、数据到来时唤醒特定goroutine的机制
sysmon线程的作用
- 执行timer,schedule函数和sysmon共同保障timer
- 抢占G和P,handle off机制
- 强制执行GC
抢占原理
- 通过栈增长代码,设置特殊标识,触发1次协程调度
- 1.14 中通过信号量实现