一个 goroutine 阻塞在系统调用里,它占着的 P 怎么办?如果 P 也跟着停了,其它 goroutine 就饿死了。这篇用只有一个 P 的最苛刻设置量出被饿的时间,再用不通知运行时的原始系统调用做对照,并读源码里 sysmon 拿走 P 的规则。
系统调用阻塞 100 µs:另一个 goroutine 开始运行的延迟 最小 42 µs / 中位 45 µs / 最大 62 µs;系统调用本身耗时中位 160 µs 系统调用阻塞 1000 µs:另一个 goroutine 开始运行的延迟 最小 10 µs / 中位 13 µs / 最大 156 µs;系统调用本身耗时中位 1511 µs 系统调用阻塞 10000 µs:另一个 goroutine 开始运行的延迟 最小 10 µs / 中位 41 µs / 最大 97 µs;系统调用本身耗时中位 13750 µs 系统调用阻塞 100000 µs:另一个 goroutine 开始运行的延迟 最小 6 µs / 中位 65 µs / 最大 223 µs;系统调用本身耗时中位 108548 µs
syscall.Syscall6 进入内核前先告诉运行时“我要进系统调用了”(entersyscall);运行时的监控线程 sysmon 发现这个 P 在系统调用里超过一个周期且有别的 goroutine 在等,就把 P 拿走交给别的线程。实验里这个交接只需几十微秒,所以只有一个 P 时,另一个 goroutine 也不会饿死。
系统调用阻塞 100 µs:另一个 goroutine 开始运行的延迟 最小 106 µs / 中位 145 µs / 最大 171 µs;系统调用本身耗时中位 144 µs 系统调用阻塞 1000 µs:另一个 goroutine 开始运行的延迟 最小 289 µs / 中位 1512 µs / 最大 1515 µs;系统调用本身耗时中位 1511 µs 系统调用阻塞 10000 µs:另一个 goroutine 开始运行的延迟 最小 26 µs / 中位 14989 µs / 最大 15026 µs;系统调用本身耗时中位 14987 µs 系统调用阻塞 100000 µs:另一个 goroutine 开始运行的延迟 最小 22798 µs / 中位 29988 µs / 最大 30102 µs;系统调用本身耗时中位 29989 µs
RawSyscall 不调用 entersyscall,运行时认为这个 goroutine 一直在运行用户代码,P 不会被拿走,另一个 goroutine 只能等它返回——延迟与系统调用时长几乎一样。这就是为什么只有确定几乎立刻返回的系统调用才适合用 raw 版本。
系统调用阻塞 100 µs:另一个 goroutine 开始运行的延迟 最小 3 µs / 中位 4 µs / 最大 48 µs;系统调用本身耗时中位 155 µs 系统调用阻塞 1000 µs:另一个 goroutine 开始运行的延迟 最小 3 µs / 中位 6 µs / 最大 9 µs;系统调用本身耗时中位 1506 µs 系统调用阻塞 10000 µs:另一个 goroutine 开始运行的延迟 最小 3 µs / 中位 10 µs / 最大 63 µs;系统调用本身耗时中位 15013 µs 系统调用阻塞 100000 µs:另一个 goroutine 开始运行的延迟 最小 7 µs / 中位 36 µs / 最大 158 µs;系统调用本身耗时中位 108533 µs
GOMAXPROCS=4 时有空闲的 P,等待的 goroutine 几乎立刻被唤醒去跑(4–36 µs),不必等 sysmon 从阻塞的线程手里拿走 P。这个差异我是从现象推的,没有读唤醒路径的代码。
proc.go:6548-6558
for {
if idle == 0 { // start with 20us sleep...
delay = 20
} else if idle > 50 { // start doubling the sleep after 1ms...
delay *= 2
}
if delay > 10*1000 { // up to 10ms
delay = 10 * 1000
}
usleep(delay)
proc.go:6743-6765
// Retake the P if it's there for more than 1 sysmon tick (at least 20us).
if syst := int64(pp.syscalltick); !sysretake && int64(pd.syscalltick) != syst {
pd.syscalltick = uint32(syst)
pd.syscallwhen = now
thread.resume()
goto done
}
// On the one hand we don't want to retake Ps if there is no other work to do,
// but on the other hand we want to retake them eventually
// because they can prevent the sysmon thread from deep sleep.
if runqempty(pp) && sched.nmspinning.Load()+sched.npidle.Load() > 0 && pd.syscallwhen+10*1000*1000 > now {
thread.resume()
goto done
}
// Take the P. Note: because we have the scan bit, the goroutine
// is at worst stuck spinning in exitsyscall.
thread.takeP()
thread.resume()
n++
// Handoff the P for some other thread to run it.源码里 sysmon 在忙的时候每 20 µs 看一次,空闲久了逐渐拉长到 10 ms;retake 的规则是 P 在系统调用里停留超过一个周期才可能被拿走,并且如果没有别的活可干,最长可以拖到 10 ms 才拿。这解释了为什么实验里等待延迟是几十微秒,而不是 10 ms——因为有一个等着运行的 goroutine。