Skip to content

Commit 1079405

Browse files
committed
runtime: add runtime.Yield
1 parent ec86954 commit 1079405

File tree

3 files changed

+247
-0
lines changed

3 files changed

+247
-0
lines changed

src/runtime/proc.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,189 @@ func Gosched() {
353353
mcall(gosched_m)
354354
}
355355

356+
// Yield cooperatively yields if, and only if, the scheduler is "busy".
357+
//
358+
// This can be called by any work wishing to utilize strictly spare capacity
359+
// while minimizing the degree to which it delays other work from being promptly
360+
// scheduled.
361+
//
362+
// Yield is intended to have very low overhead, particularly in its no-op case
363+
// where there is idle capacity in the scheduler and the caller does not need to
364+
// yield. This should allow it to be called often, such as in the body of tight
365+
// loops, in any tasks wishing to yield promptly to any waiting work.
366+
//
367+
// When there is waiting work, the yielding goroutine may briefly be rescheduled
368+
// after it, or may, in some cases, be parked in a waiting 'yield' state until
369+
// the scheduler next has spare capacity to resume it. Yield does not guarantee
370+
// fairness or starvation-prevention: once a goroutine Yields(), it may remain
371+
// parked until the scheduler next has idle capacity. This means Yield can block
372+
// for unbounded durations in the presence of sustained over-saturation; callers
373+
// are responsible for deciding where to Yield() to avoid priority inversions.
374+
//
375+
// Yield will never park if the calling goroutine is locked to an OS thread.
376+
func Yield() {
377+
// Common/fast case: do nothing if npidle is non-zero meaning there is
378+
// an idle P so no reason to yield this one. Doing only this check here keeps
379+
// Yield inlineable (~70 of 80 as of writing).
380+
if sched.npidle.Load() == 0 {
381+
maybeYield()
382+
}
383+
}
384+
385+
// maybeYield is called by Yield if npidle is zero, meaning there are no idle Ps
386+
// and thus there may be work to which the caller should yield. Such work could
387+
// be on this local runq of the caller's P, on the global runq, in the runq of
388+
// some other P, or even in the form of ready conns waiting to be noticed by a
389+
// netpoll which would then ready runnable goroutines.
390+
//
391+
// Keeping this function extremely cheap is essential: it must be cheap enough
392+
// that callers can call it in very tight loops, as very frequent calls ensure a
393+
// task wishing to yield when work is waiting will do so promptly. Checking the
394+
// runq of every P or calling netpoll are too expensive to do in every call, so
395+
// given intent is to bound how long work may wait, such checks only need to be
396+
// performed after some amount of time has elapsed (e.g. 0.25ms). To minimize
397+
// overhead when called at a higher frequency, this elapsed time is checked with
398+
// an exponential backoff.
399+
//
400+
// runqs are checked directly with non-atomic reads rather than runqempty: being
401+
// cheap is our top priority and a microsecond of staleness is fine as long as
402+
// the check does not get optimized out of a calling loop body (hence noinline).
403+
//
404+
//go:noinline
405+
func maybeYield() {
406+
gp := getg()
407+
408+
// Don't park while locked to an OS thread.
409+
if gp.lockedm != 0 {
410+
return
411+
}
412+
413+
// If the local P's runq ring buffer/next is non-empty, yield to waiting G.
414+
if p := gp.m.p.ptr(); p.runqhead != p.runqtail || p.runnext != 0 {
415+
// If there is work in the local P's runq, we can yield by just going to the
416+
// back of the local P's runq via goyield: this achieves the same goal of
417+
// letting waiting work run instead of us, but without parking on the global
418+
// yieldq and potentially switching Ps. While that's our preferred choice,
419+
// we want to avoid thrashing back and forth between multiple Yield-calling
420+
// goroutines: in such a case it is better to just park one so the other
421+
// stops seeing it in the queue and yielding to it. To detect and break this
422+
// cycle, we put a 1 in the yieldchecks field: if the other goroutine yields
423+
// right back, but is then still in this runq bringing us here again, we'll
424+
// see this 1 and park instead. We can clobber yieldchecks here since we're
425+
// actively yielding -- we don't need the counter to decide to do so. And
426+
// our sentinel will in turn be clobbered the very next time the time is put
427+
// in the upper bits, which it will be when they're zero if we don't yield,
428+
// so this sentinel should be relatively reliable in indicating thrashing.
429+
if gp.yieldchecks == 1 {
430+
yieldPark()
431+
return
432+
}
433+
gp.yieldchecks = 1
434+
// Go to the back of the local runq.
435+
goyield()
436+
return
437+
}
438+
439+
// If the global runq is non-empty, park in the global yieldq right away: that
440+
// is work someone needs to pick up and it might as well be our P. We could,
441+
// potentially, directly claim it here and goyield or equivalently to try to
442+
// remain on this P, but just parking and letting this P go to findRunnable
443+
// avoid duplication of its logic and seems good enough.
444+
if !sched.runq.empty() {
445+
yieldPark()
446+
return
447+
}
448+
449+
// We didn't find anything via cheap O(1) checks of our runq or global runq but
450+
// it is possible there are goroutines waiting in runqs of other Ps that are
451+
// not being stolen by an idle P -- the lack of idle Ps (npidle=0) is what got
452+
// us here. Furthermore, given the lack of idle Ps, it is also possible that
453+
// ready conns are waiting for a netpoll to notice them and ready their
454+
// goroutines i.e. work to which we should then yield. However, searching all
455+
// runqs, and even more so netpoll, is too expensive for every maybeYield
456+
// call: being extremely low overhead is essential to allowing Yield() to be
457+
// called at high enough frequency to make the caller respond to changing load
458+
// promptly.
459+
//
460+
// Given our main goal here is to reduce/bound *how long* work waits, we can
461+
// do more extensive/expensive checks searching all runqs / netpoll less often
462+
// so long as we do them often enough. While we can't
463+
// define "enough" in term of a number of calls or probabilistic fraction of
464+
// calls (e.g. cheaprand()&1023==0) due to variability in caller frequency, we
465+
// can frame it in terms of elapsed time: so long as we check for waiting work
466+
// after some amount of time has elapsed, we bound how long it waits. We
467+
// choose approximately a quarter millisecond for this time: this is long
468+
// enough that it should make call overhead negligible, while still being a
469+
// duration smaller than the latency of any typical network requests.
470+
//
471+
// Checking nanotime() every call to implement this cap would in and of itself
472+
// be too expensive however, so we instead check the time with an exponential
473+
// backoff, using a simple call counter. We combine this counter and the last-
474+
// check time in uint32 field on G: 11 lower bits store the counter while the
475+
// 21 higher bits store the time as nanos quantized to a 0.25ms "epoch" by
476+
// discarding the lower 18 bits of a int64 nanotime() value. When the counter
477+
// is 2^k - 1, we check the time; if the 'epoch' has changed, we do the extended
478+
// search for waiting work. If the counter is about to overflow but the epoch
479+
// hasn't changed, we reset it to half-max to keep checking at the backed-off
480+
// rate. Note that while we discard 18 bits to quantize, since the
481+
// counter is in the low 11, we only shift by the difference and just mask the
482+
// rest out.
483+
const yieldCountBits, yieldCountMask = 11, (1 << 11) - 1
484+
const yieldEpochShift = 18 - yieldCountBits
485+
gp.yieldchecks++
486+
// Exp-backoff using 2^k-1 as when we check.
487+
if count := gp.yieldchecks & yieldCountMask; (count & (count + 1)) == 0 {
488+
prev := gp.yieldchecks &^ yieldCountMask
489+
now := uint32(nanotime()>>yieldEpochShift) &^ yieldCountMask
490+
if now != prev {
491+
// Set yieldchecks to just new high timestamp bits, cleaning counter.
492+
gp.yieldchecks = now
493+
494+
// Check runqs of all Ps; if we find anything park free this P to steal.
495+
for i := range allp {
496+
// We don't need the extra accuracy (and cost) of runqempty here either;
497+
// Worst-case we'll yield a check later or maybe park and unpark.
498+
if allp[i].runqhead != allp[i].runqtail || allp[i].runnext != 0 {
499+
yieldPark()
500+
return
501+
}
502+
}
503+
504+
// Check netpoll; a ready conn is basically a runnable goroutine which we
505+
// would yield to if we saw it, but the lack of idle Ps may mean nobody is
506+
// checking this as often right now and there may be ready conns waiting.
507+
if netpollinited() && netpollAnyWaiters() && sched.lastpoll.Load() != 0 {
508+
var found bool
509+
systemstack(func() {
510+
if list, delta := netpoll(0); !list.empty() {
511+
injectglist(&list)
512+
netpollAdjustWaiters(delta)
513+
found = true
514+
}
515+
})
516+
if found {
517+
goyield()
518+
}
519+
}
520+
} else if count == yieldCountMask {
521+
// Counter overflow before hitting time; reset half way back.
522+
gp.yieldchecks = prev | (yieldCountMask / 2)
523+
}
524+
}
525+
}
526+
527+
// yieldPark parks the current goroutine in a waiting state with reason yield
528+
// and puts it in the yieldq queue for findRunnable. A goroutine that has to
529+
// park to Yield is considered "waiting" rather than "runnable" as it is blocked
530+
// in this state until there is strictly spare execution capacity available to
531+
// resume it, unlike runnable goroutines which generally take runs running at
532+
// regular intervals. A parked yielded goroutine is more like being blocked on
533+
// a cond var or lock that will be signaled when we next detect spare capacity.
534+
func yieldPark() {
535+
checkTimeouts()
536+
gopark(yield_put, nil, waitReasonYield, traceBlockPreempted, 1)
537+
}
538+
356539
// goschedguarded yields the processor like gosched, but also checks
357540
// for forbidden states and opts out of the yield in those cases.
358541
//
@@ -3445,6 +3628,23 @@ top:
34453628
}
34463629
}
34473630

3631+
// Nothing runnable, so check for yielded goroutines parked in yieldq.
3632+
if !sched.yieldq.empty() {
3633+
lock(&sched.lock)
3634+
bg := sched.yieldq.pop()
3635+
unlock(&sched.lock)
3636+
if bg != nil {
3637+
trace := traceAcquire()
3638+
casgstatus(bg, _Gwaiting, _Grunnable)
3639+
if trace.ok() {
3640+
// Match other ready paths for trace visibility.
3641+
trace.GoUnpark(bg, 0)
3642+
traceRelease(trace)
3643+
}
3644+
return bg, false, false
3645+
}
3646+
}
3647+
34483648
// We have nothing to do.
34493649
//
34503650
// If we're in the GC mark phase, can safely scan and blacken objects,
@@ -3509,6 +3709,12 @@ top:
35093709
unlock(&sched.lock)
35103710
return gp, false, false
35113711
}
3712+
3713+
// Re-check yieldq again, this time while holding sched.lock.
3714+
if !sched.yieldq.empty() {
3715+
unlock(&sched.lock)
3716+
goto top
3717+
}
35123718
if !mp.spinning && sched.needspinning.Load() == 1 {
35133719
// See "Delicate dance" comment below.
35143720
mp.becomeSpinning()
@@ -7111,6 +7317,20 @@ func (q *gQueue) popList() gList {
71117317
return stack
71127318
}
71137319

7320+
// yield_put is the gopark unlock function for Yield. It enqueues the goroutine
7321+
// onto the global yield queue. Returning true keeps the G parked until another
7322+
// part of the scheduler makes it runnable again. The G remains in _Gwaiting
7323+
// after this returns. Nothing else will find/ready this G in the interim since
7324+
// it isn't on a runq until we put it on the yieldq for findRunnable to find.
7325+
//
7326+
//go:nosplit
7327+
func yield_put(gp *g, _ unsafe.Pointer) bool {
7328+
lock(&sched.lock)
7329+
sched.yieldq.pushBack(gp)
7330+
unlock(&sched.lock)
7331+
return true
7332+
}
7333+
71147334
// A gList is a list of Gs linked through g.schedlink. A G can only be
71157335
// on one gQueue or gList at a time.
71167336
type gList struct {

src/runtime/proc_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,25 @@ func TestYieldLocked(t *testing.T) {
103103
<-c
104104
}
105105

106+
func TestYield(t *testing.T) {
107+
var wg sync.WaitGroup
108+
start := make(chan struct{})
109+
for i := 0; i < runtime.GOMAXPROCS(0)*2; i++ {
110+
wg.Add(1)
111+
go func() {
112+
defer wg.Done()
113+
<-start
114+
for j := 0; j < 1000; j++ {
115+
if i%2 == 0 || j == 999 {
116+
runtime.Yield()
117+
}
118+
}
119+
}()
120+
}
121+
close(start)
122+
wg.Wait()
123+
}
124+
106125
func TestGoroutineParallelism(t *testing.T) {
107126
if runtime.NumCPU() == 1 {
108127
// Takes too long, too easy to deadlock, etc.

src/runtime/runtime2.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,8 @@ type g struct {
512512
lastsched int64 // timestamp when the G last started running
513513
runningnanos int64 // wall time spent in the running state
514514

515+
yieldchecks uint32 // a packed approx time and count of maybeYield checks; see Yield().
516+
515517
// goroutineProfiled indicates the status of this goroutine's stack for the
516518
// current in-progress goroutine profile
517519
goroutineProfiled goroutineProfileStateHolder
@@ -805,6 +807,10 @@ type schedt struct {
805807
runq gQueue
806808
runqsize int32
807809

810+
// Global background-yield queue: goroutines that voluntarily yielded
811+
// while the scheduler was busy. Does NOT contribute to runqsize.
812+
yieldq gQueue
813+
808814
// disable controls selective disabling of the scheduler.
809815
//
810816
// Use schedEnableUser to control this.
@@ -1099,6 +1105,7 @@ const (
10991105
waitReasonTraceProcStatus // "trace proc status"
11001106
waitReasonPageTraceFlush // "page trace flush"
11011107
waitReasonCoroutine // "coroutine"
1108+
waitReasonYield // "yield"
11021109
waitReasonGCWeakToStrongWait // "GC weak to strong wait"
11031110
)
11041111

@@ -1140,6 +1147,7 @@ var waitReasonStrings = [...]string{
11401147
waitReasonTraceProcStatus: "trace proc status",
11411148
waitReasonPageTraceFlush: "page trace flush",
11421149
waitReasonCoroutine: "coroutine",
1150+
waitReasonYield: "yield",
11431151
waitReasonGCWeakToStrongWait: "GC weak to strong wait",
11441152
}
11451153

0 commit comments

Comments
 (0)