一个没有任何函数调用的死循环,调度器怎么把它打断?老版本的 Go 打不断,整个程序会被一个循环卡死。这篇关掉信号抢占做对照,量出默认设置下被打断需要多久、GC 会不会被拖住,并读源码里的安全点说明。
main 每次 time.Sleep(10ms) 实际耗时:最小 25.2 ms / 中位 30.0 ms / 最大 30.1 ms(30 次,GOMAXPROCS=1,GODEBUG="") (超时:15 s 内没有结束,被我杀掉了;GOMAXPROCS=1,GODEBUG='asyncpreemptoff=1')
同步抢占靠函数序言里的栈检查:运行时把 goroutine 的栈边界改成一个必定失败的值,下次进函数就会发现“该让出了”。但上面这个循环的汇编里没有任何调用、也没有栈检查,它从来不会走进那段代码。
死循环 goroutine 在跑时调用 runtime.GC():耗时 0.2 ms(GOMAXPROCS=2,GODEBUG="") (超时:4 s 内没有结束,被我杀掉了;GOMAXPROCS=2,GODEBUG='asyncpreemptoff=1')
加上 GODEBUG=asyncpreemptoff=1 后,main 的 Sleep 到期也拿不到唯一的 P,程序 15 秒内没有结束;GC 的情形里 runtime.GC() 4 秒内没有返回。我杀掉了它们,所以只能说“超过这个时间都没结束”,不是证明永远不结束。
死循环的 goroutine 占着唯一的 P 跑了约 1 秒:收到 SIGURG 19 次(GOMAXPROCS=1,GODEBUG="") (超时:4 s 内没有结束,被我杀掉了;GOMAXPROCS=1,GODEBUG='asyncpreemptoff=1')
默认情况下运行时会给占着线程的死循环发信号(SIGURG),在一条安全的指令上把它停住。实验里一个死循环占着 P 约 1 秒,订阅 SIGURG 的 os/signal 收到 19 次;main 的 10 ms 睡眠实际 25–30 ms 才醒——比 10 ms 慢,但不会被卡死。慢多少、为什么是这个数,我没有深查,源码里能确认的只有每个 G 有 10 ms 的时间片。
main.spinNoCall STEXT size=48 align=0x0 args=0x0 locals=0x0 funcid=0x0 leaf
(<gtmp>/ap2/main.go:16) TEXT main.spinNoCall(SB), LEAF|NOFRAME|ABIInternal, $0-0
(<gtmp>/ap2/main.go:16) FUNCDATA $0, gclocals·g5+hNtRBP6YXNjfog7aZjQ==(SB)
(<gtmp>/ap2/main.go:16) FUNCDATA $1, gclocals·g5+hNtRBP6YXNjfog7aZjQ==(SB)
(<gtmp>/ap2/main.go:19) MOVD ZR, R0
(<gtmp>/ap2/main.go:20) ADD $1, R0, R0
(<gtmp>/ap2/main.go:21) TST $268435455, R0
(<gtmp>/ap2/main.go:21) BNE 4
(<unknown line number>) NOP
(<GOROOT>/src/sync/atomic/type.go:19) MOVD $main.stop(SB), R1
(<GOROOT>/src/sync/atomic/type.go:19) LDARW (R1), R1
(<gtmp>/ap2/main.go:21) CBZW R1, 4
(<gtmp>/ap2/main.go:21) RET (R30)
0x0000 e0 03 1f aa 00 04 00 91 1f 6c 40 f2 c1 ff ff 54 .........l@....T
0x0010 01 00 00 90 21 00 00 91 21 fc df 88 41 ff ff 34 ....!...!...A..4
这个函数的汇编里 CALL 指令的个数:0(上面是整个函数:LEAF|NOFRAME,没有栈检查的序言,也没有任何调用)GC 的停顿阶段要让所有 goroutine 停下来。默认设置下死循环在跑时 runtime.GC() 只花了 0.2 ms;关掉信号抢占就等不到。源码里 asyncpreemptoff 的说明也提到这会让某些循环长时间不可抢占、可能延迟 GC。
preempt.go:7-22
// A goroutine can be preempted at any safe-point. Currently, there
// are a few categories of safe-points:
//
// 1. A blocked safe-point occurs for the duration that a goroutine is
// descheduled, blocked on synchronization, or in a system call.
//
// 2. Synchronous safe-points occur when a running goroutine checks
// for a preemption request.
//
// 3. Asynchronous safe-points occur at any instruction in user code
// where the goroutine can be safely paused and a conservative
// stack and register scan can find stack roots. The runtime can
// stop a goroutine at an async safe-point using a signal.
//
// At both blocked and synchronous safe-points, a goroutine's CPU
// state is minimal and the garbage collector has complete information
proc.go:6677-6679
// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms
extern.go:227-229
asyncpreemptoff: asyncpreemptoff=1 disables signal-based
asynchronous goroutine preemption. This makes some loops
non-preemptible for long periods, which may delay GC and
signal_unix.go:44-45
// sigPreempt is the signal used for non-cooperative preemption.
//preempt.go 开头把安全点分成三类:阻塞、同步(函数序言)、异步(用户代码任意指令,用信号停住并检查这条指令是否安全)。异步抢占补上了前两类够不到的地方。具体“哪些指令算安全”的判断我没有读。