详解协程抢占式调度
函数调用间进行抢占式调度
假设我们现在有这样一个协程,它会进行函数嵌套调用,代码如下所示:
func foo1() { fmt.Println("foo1调用foo2") foo2() } func foo2() { fmt.Println("foo2调用foo3") foo3() } func foo3() { fmt.Println("foo3") } func main() { //设置WaitGroup等待协程运行结束 var wg sync.WaitGroup wg.Add(1) //通过协程调用foo1 go func() { defer wg.Done() foo1() }() //等待协程运行结束 wg.Wait() }
我们给出运行结果:
foo1调用foo2
foo2调用foo3
foo3
基于这段代码示例,我们通过这段指令获取plan9
汇编码:
go build -gcflags -S main.go
可以看到在foo1
插入runtime.morestack_noctxt
方法,该方法是用于检查当前协程是否有足够的堆栈空间以保证函数的正常调用,基于这一点,go
就会在进行这部检查时顺带检查协程的执行时长,一旦超过10ms
该方法就会将协程设置为标记可被抢占:
0x0061 00097 (F:\github\test\main.go:8) CALL runtime.morestack_noctxt(SB)
如下图,我们的调用的函数都会被插入一个morestack
通过这个标记判断当前协程执行耗时,一旦发现超过10ms则会直接通过抢占式调度的方法g0
协程直接调用schedule
方法获取另外的协程进行调用:
这一点我们可以在asm_amd64.s
看到morestack
的newstack
的代码,而newstack就是实现抢占式调度的核心:
TEXT runtime·morestack(SB),NOSPLIT,$0-0 // Cannot grow scheduler stack (m->g0). get_tls(CX) MOVQ g(CX), BX MOVQ g_m(BX), BX MOVQ m_g0(BX), SI CMPQ g(CX), SI JNE 3(PC) CALL runtime·badmorestackg0(SB) CALL runtime·abort(SB) //...... //函数调用前会调用newstack进行抢占式的检查 CALL runtime·newstack(SB) CALL runtime·abort(SB) // crash if newstack returns RET
上述的newstack
方法在stack.go
中,如果当前协程可被抢占则会调用gopreempt_m
回到g0
调用schedule
方法从协程队列中拿到新的协程执行任务:
func newstack() { preempt := stackguard0 == stackPreempt //如果preempt 为true,则直接当前协程被标记为抢占直接调用gopreempt_m让出线程执行权 if preempt { if gp == thisg.m.g0 { throw("runtime: preempt g0") } //...... // Act like goroutine called runtime.Gosched. gopreempt_m(gp) // never return } }
基于系统调用发起信号的抢占式调度
假设我们的协程没有进行额外的函数调用,是否就意味着当前协程的线程不能被抢占呢?很明显不是这样:
网络传输过程中需要发送某些紧急消息希望通过已有连接迅速将消息通知给对端时,就会产生SIGURG
信号,go
语言就会在收到此信号时触发抢占式调度。
进行GC
工作时像目标线程发送信号由此实现抢占式调度。
对于第一点我们可以在signal_unix.go
的sighandler
方法得以印证,可以看到它会判断sig 是否为_SIGURG
若是则调用doSigPreempt
进行抢占式调度
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { //如果传入的信号为_SIGURG则调用doSigPreempt回到schedule实现抢占式调度 if sig == sigPreempt && debug.asyncpreemptoff == 0 && !delayedSignal { // Might be a preemption signal. doSigPreempt(gp, c) } //...... }
doSigPreempt
会通过调用asyncPreempt
最终执行到preempt.go
的asyncPreempt2
调用到和上文函数调用抢占式调度方法gopreempt_m
回到schedule
方法从而完成抢占式调度:
func doSigPreempt(gp *g, ctxt *sigctxt) { //...... if wantAsyncPreempt(gp) { if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok { // 调用asyncPreempt内部会得到一个和上文函数调用时抢占式调度的方法gopreempt_m的调用从而回到schedule方法 ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc) } } //...... }