Since go1.14, cpu busy goroutine will not occupy a P indefinitely any more. This is done by a cool feature: asynchronously preemption.
There is one extra NoP thread(sysmon) runs time to time. It checks if current goroutine has run 10+ms. If so, signal the M runs this goroutine to switch to another goroutine. The function calling graph looks like this:
sysmon() {
for {
usleep(delay)
retake() {
if schedwhen + 10ms <= now {
preemptone()
}
}
}
}
schedwhen is the time when the goroutine was scheduled to use cpu. So I conclude that Go prevents goroutine from running more than 10ms. However, My local cpu-bound test shows almost all goroutine run 10+ms successively. It seems avg 20ms. See attachments for test code and trace.
The reason is usleep(delay). Preemption checking happens every 10ms. this is a less frequent sampling rate. To achieve 10ms time slice, the checking should happens less than every 5ms. For example, background forcegc goroutine does check gcTrigger every forcegcperiod/2 to guarantee that GC happens at least once every forcegcperiod.
How long a goroutine could run by design?
If its 10ms, sysmon should sleep less than 5ms.
If its 20ms, forecePreemptNs should be doubled.