GO 调度机制

调度器初始化

  1. 初始化G0栈
    MOVQ $runtime·g0(SB), DI
  2. m0和g0相互绑定
    // save m->g0 = g0 MOVQ CX, m_g0(AX)
    // save m0 to g0->m MOVQ AX, g_m(CX)
  3. 暂存命令行参数
    CALL runtime·args(SB)
  4. 获取cpu核心数
    CALL runtime·osinit(SB)
  5. 初始化调度函数,会初始化P,P0绑定M0
    CALL runtime·schedinit(SB)
  6. 创建主G,执行runtime.main
    CALL runtime·newproc(SB)
  7. 开启调度循环,里面会调用schedule函数 CALL runtime·mstart(SB)
  8. 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 中通过信号量实现

results matching ""

    No results matching ""