Go 语言实现——异步抢占调度

Go 1.14 中添加了异步抢占调度:

Goroutines are now asynchronously preemptible. As a result, loops without function calls no longer potentially deadlock the scheduler or significantly delay garbage collection. This is supported on all platforms except windows/arm, darwin/arm, js/wasm, and plan9/*.

https://golang.org/doc/go1.14#runtime

这个功能主要是解决 golang-internals-goroutine 切换一节中曾经说过的 goroutine 如果一直不调用函数,那么它就不会被抢占 问题。

顺着 之前的解析 往下看,diff 一下可以看到, preemptone 比之前的代码增加了下面的部分:

 func preemptone(_p_ *p) bool {
     mp := _p_.m.ptr()
     if mp == nil || mp == getg().m {
         return false
     }
     gp := mp.curg
     if gp == nil || gp == mp.g0 {
         return false
     }

     gp.preempt = true

     gp.stackguard0 = stackPreempt

+    if preemptMSupported && debug.asyncpreemptoff == 0 {
+        _p_.preempt = true
+        preemptM(mp)
+    }

     return true
 }

这个 preemptM 做的就是使用 tgkill 系统调用给要抢占的线程发送一个信号,literally。

int tgkill(int tgid, int tid, int sig);

tgkill() sends the signal sig to the thread with the thread ID tid in the thread group tgid. (By contrast, kill(2) can only be used to send a signal to a process (i.e., thread group) as a whole, and the signal will be delivered to an arbitrary thread within that process.)

https://linux.die.net/man/2/tgkill

func preemptM(mp *m) {
    if atomic.Cas(&mp.signalPending, 0, 1) {
        signalM(mp, sigPreempt)
    }
}

func signalM(mp *m, sig int) {
    if atomic.Load(&touchStackBeforeSignal) != 0 {
        atomic.Cas((*uint32)(unsafe.Pointer(mp.gsignal.stack.hi-4)), 0, 0)
    }
    tgkill(getpid(), int(mp.procid), sig)
}

信号处理函数在收到该信号后,会制造一个在当前指令处调用 asyncPreempt 函数的现场,这样在信号函数处理完,goroutine 返回执行后其会从 asyncPreempt 函数开始往下执行。

func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
    c := &sigctxt{info, ctxt}
    //...
    if sig == sigPreempt {
        doSigPreempt(gp, c)
    }
    //...
}

func doSigPreempt(gp *g, ctxt *sigctxt) {
    if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) {
        ctxt.pushCall(funcPC(asyncPreempt))
    }
}

func (c *sigctxt) pushCall(targetPC uintptr) {
    // 保存当前运行 goroutine 的指令指针寄存器到栈上,然后将指针指向 asyncPreempt
    // 这样就在当前指令处强制插入了一个函数调用 asyncPreempt,信号处理函数结束后 goroutine
    // 会从 asyncPreempt 开始执行
    pc := uintptr(c.rip())
    sp := uintptr(c.rsp())
    sp -= sys.PtrSize
    *(*uintptr)(unsafe.Pointer(sp)) = pc
    c.set_rsp(uint64(sp))
    c.set_rip(uint64(targetPC))
}

asyncPreempt 函数为保存当前的各种寄存器,然后调用 asyncPreempt2 ,这个函数中会调用调度相关的函数抢占当前线程给其它 goroutine 去执行。

TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0
    ...
    MOVQ AX, 0(SP)
    MOVQ CX, 8(SP)
    ...
    MOVUPS X15, 352(SP)
    CALL ·asyncPreempt2(SB)
    MOVUPS 352(SP), X15
    ...
    MOVQ 8(SP), CX
    MOVQ 0(SP), AX
    ADJSP $-368
    POPFQ
    POPQ BP
    RET