runtime: refactor work stealing to dedicated function

findrunnable has grown very large and hard to follow over the years.
Parts we can split out into logical chunks should help make it more
understandable and easier to change in the future.

The work stealing loop is one such big chunk that is fairly trivial to
split out into its own function, and even has the advantage of
simplifying control flow by removing a goto around work stealing.

This CL should have no functional changes.

For #43997.
For #44313.

Change-Id: Ie69670c7bc60bd6c114e860184918717829adb22
Reviewed-on: https://go-review.googlesource.com/c/go/+/307913
Trust: Michael Pratt <mpratt@google.com>
Run-TryBot: Michael Pratt <mpratt@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Chris Hines <chris.cs.guy@gmail.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
This commit is contained in:
Michael Pratt 2021-02-16 15:50:49 -05:00
parent 9dd71ba913
commit 60ab197bc2

View file

@ -2681,85 +2681,40 @@ top:
} }
} }
// Steal work from other P's. // Spinning Ms: steal work from other Ps.
//
// Limit the number of spinning Ms to half the number of busy Ps.
// This is necessary to prevent excessive CPU consumption when
// GOMAXPROCS>>1 but the program parallelism is low.
procs := uint32(gomaxprocs) procs := uint32(gomaxprocs)
ranTimer := false if _g_.m.spinning || 2*atomic.Load(&sched.nmspinning) < procs-atomic.Load(&sched.npidle) {
// If number of spinning M's >= number of busy P's, block. if !_g_.m.spinning {
// This is necessary to prevent excessive CPU consumption _g_.m.spinning = true
// when GOMAXPROCS>>1 but the program parallelism is low. atomic.Xadd(&sched.nmspinning, 1)
if !_g_.m.spinning && 2*atomic.Load(&sched.nmspinning) >= procs-atomic.Load(&sched.npidle) { }
goto stop
}
if !_g_.m.spinning {
_g_.m.spinning = true
atomic.Xadd(&sched.nmspinning, 1)
}
const stealTries = 4
for i := 0; i < stealTries; i++ {
stealTimersOrRunNextG := i == stealTries-1
for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() { gp, inheritTime, tnow, w, newWork := stealWork(now)
if sched.gcwaiting != 0 { now = tnow
goto top if gp != nil {
} // Successfully stole.
p2 := allp[enum.position()] return gp, inheritTime
if _p_ == p2 { }
continue if newWork {
} // There may be new timer or GC work; restart to
// discover.
// Steal timers from p2. This call to checkTimers is the only place goto top
// where we might hold a lock on a different P's timers. We do this }
// once on the last pass before checking runnext because stealing if w != 0 && (pollUntil == 0 || w < pollUntil) {
// from the other P's runnext should be the last resort, so if there // Earlier timer to wait for.
// are timers to steal do that first. pollUntil = w
//
// We only check timers on one of the stealing iterations because
// the time stored in now doesn't change in this loop and checking
// the timers for each P more than once with the same value of now
// is probably a waste of time.
//
// timerpMask tells us whether the P may have timers at all. If it
// can't, no need to check at all.
if stealTimersOrRunNextG && timerpMask.read(enum.position()) {
tnow, w, ran := checkTimers(p2, now)
now = tnow
if w != 0 && (pollUntil == 0 || w < pollUntil) {
pollUntil = w
}
if ran {
// Running the timers may have
// made an arbitrary number of G's
// ready and added them to this P's
// local run queue. That invalidates
// the assumption of runqsteal
// that is always has room to add
// stolen G's. So check now if there
// is a local G to run.
if gp, inheritTime := runqget(_p_); gp != nil {
return gp, inheritTime
}
ranTimer = true
}
}
// Don't bother to attempt to steal if p2 is idle.
if !idlepMask.read(enum.position()) {
if gp := runqsteal(_p_, p2, stealTimersOrRunNextG); gp != nil {
return gp, false
}
}
} }
} }
if ranTimer {
// Running a timer may have made some goroutine ready.
goto top
}
stop: // We have nothing to do.
//
// We have nothing to do. If we're in the GC mark phase, can // If we're in the GC mark phase, can safely scan and blacken objects,
// safely scan and blacken objects, and have work to do, run // and have work to do, run idle-time marking rather than give up the
// idle-time marking rather than give up the P. // P.
if gcBlackenEnabled != 0 && gcMarkWorkAvailable(_p_) { if gcBlackenEnabled != 0 && gcMarkWorkAvailable(_p_) {
node := (*gcBgMarkWorkerNode)(gcBgMarkWorkerPool.pop()) node := (*gcBgMarkWorkerNode)(gcBgMarkWorkerPool.pop())
if node != nil { if node != nil {
@ -3008,6 +2963,81 @@ func pollWork() bool {
return false return false
} }
// stealWork attempts to steal a runnable goroutine or timer from any P.
//
// If newWork is true, new work may have been readied.
//
// If now is not 0 it is the current time. stealWork returns the passed time or
// the current time if now was passed as 0.
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
pp := getg().m.p.ptr()
ranTimer := false
const stealTries = 4
for i := 0; i < stealTries; i++ {
stealTimersOrRunNextG := i == stealTries-1
for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
if sched.gcwaiting != 0 {
// GC work may be available.
return nil, false, now, pollUntil, true
}
p2 := allp[enum.position()]
if pp == p2 {
continue
}
// Steal timers from p2. This call to checkTimers is the only place
// where we might hold a lock on a different P's timers. We do this
// once on the last pass before checking runnext because stealing
// from the other P's runnext should be the last resort, so if there
// are timers to steal do that first.
//
// We only check timers on one of the stealing iterations because
// the time stored in now doesn't change in this loop and checking
// the timers for each P more than once with the same value of now
// is probably a waste of time.
//
// timerpMask tells us whether the P may have timers at all. If it
// can't, no need to check at all.
if stealTimersOrRunNextG && timerpMask.read(enum.position()) {
tnow, w, ran := checkTimers(p2, now)
now = tnow
if w != 0 && (pollUntil == 0 || w < pollUntil) {
pollUntil = w
}
if ran {
// Running the timers may have
// made an arbitrary number of G's
// ready and added them to this P's
// local run queue. That invalidates
// the assumption of runqsteal
// that it always has room to add
// stolen G's. So check now if there
// is a local G to run.
if gp, inheritTime := runqget(pp); gp != nil {
return gp, inheritTime, now, pollUntil, ranTimer
}
ranTimer = true
}
}
// Don't bother to attempt to steal if p2 is idle.
if !idlepMask.read(enum.position()) {
if gp := runqsteal(pp, p2, stealTimersOrRunNextG); gp != nil {
return gp, false, now, pollUntil, ranTimer
}
}
}
}
// No goroutines found to steal. Regardless, running a timer may have
// made some goroutine ready that we missed. Indicate the next timer to
// wait for.
return nil, false, now, pollUntil, ranTimer
}
// wakeNetPoller wakes up the thread sleeping in the network poller if it isn't // wakeNetPoller wakes up the thread sleeping in the network poller if it isn't
// going to wake up before the when argument; or it wakes an idle P to service // going to wake up before the when argument; or it wakes an idle P to service
// timers and the network poller if there isn't one already. // timers and the network poller if there isn't one already.
@ -3252,7 +3282,7 @@ func dropg() {
// checkTimers runs any timers for the P that are ready. // checkTimers runs any timers for the P that are ready.
// If now is not 0 it is the current time. // If now is not 0 it is the current time.
// It returns the current time or 0 if it is not known, // It returns the passed time or the current time if now was passed as 0.
// and the time when the next timer should run or 0 if there is no next timer, // and the time when the next timer should run or 0 if there is no next timer,
// and reports whether it ran any timers. // and reports whether it ran any timers.
// If the time when the next timer should run is not 0, // If the time when the next timer should run is not 0,