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/*.
这个功能主要是解决 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.)
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