Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go协程抢占式调度

浅析go语言如何实现协程的抢占式调度的

作者:shark_chili

go语言通过GMP模型实现协程并发,为了避免单协程持续持有线程导致线程队列中的其他协程饥饿问题,设计者提出了一个抢占式调度机制,本文会基于一个简单的代码示例对抢占式调度过程进行深入讲解剖析

详解协程抢占式调度

函数调用间进行抢占式调度

假设我们现在有这样一个协程,它会进行函数嵌套调用,代码如下所示:

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看到morestacknewstack的代码,而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.gosighandler方法得以印证,可以看到它会判断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.goasyncPreempt2调用到和上文函数调用抢占式调度方法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)
  }
 }

 //......
}

到此这篇关于浅析go语言如何实现协程的抢占式调度的的文章就介绍到这了,更多相关go协程抢占式调度内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文