Skip to content

Commit a642b54

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

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

src/runtime/proc.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,120 @@ 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 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 p := gp.m.p.ptr(); p.runqhead != p.runqtail || p.runnext != 0 {
414+
if gp.yieldchecks == 1 {
415+
yieldPark()
416+
return
417+
}
418+
// To avoid thrashing between yields, set yieldchecks to 1: if we yield
419+
// right back and see this sentinel we'll park instead to break the cycle.
420+
gp.yieldchecks = 1
421+
goyield()
422+
return
423+
}
424+
425+
// If the global runq is non-empty, park in the global yieldq right away.
426+
if !sched.runq.empty() {
427+
yieldPark()
428+
return
429+
}
430+
431+
const yieldCountBits, yieldCountMask = 11, (1 << 11) - 1
432+
const yieldEpochShift = 18 - yieldCountBits
433+
gp.yieldchecks++
434+
if count := gp.yieldchecks & yieldCountMask; (count & (count + 1)) == 0 {
435+
prev := gp.yieldchecks &^ yieldCountMask
436+
now := uint32(nanotime()>>yieldEpochShift) &^ yieldCountMask
437+
if now != prev || count == yieldCountMask {
438+
gp.yieldchecks = now
439+
440+
for i := range allp {
441+
// We don't need the extra accuracy (and cost) of runqempty here either.
442+
if allp[i].runqhead != allp[i].runqtail || allp[i].runnext != 0 {
443+
yieldPark()
444+
return
445+
}
446+
}
447+
448+
if netpollinited() && netpollAnyWaiters() && sched.lastpoll.Load() != 0 {
449+
var found bool
450+
systemstack(func() {
451+
if list, delta := netpoll(0); !list.empty() {
452+
injectglist(&list)
453+
netpollAdjustWaiters(delta)
454+
found = true
455+
}
456+
})
457+
if found {
458+
goyield()
459+
}
460+
}
461+
}
462+
}
463+
}
464+
465+
func yieldPark() {
466+
checkTimeouts()
467+
gopark(yield_put, nil, waitReasonYield, traceBlockPreempted, 1)
468+
}
469+
356470
// goschedguarded yields the processor like gosched, but also checks
357471
// for forbidden states and opts out of the yield in those cases.
358472
//
@@ -3445,6 +3559,23 @@ top:
34453559
}
34463560
}
34473561

3562+
// Nothing runnable, so check for yielded goroutines parked in yieldq.
3563+
if !sched.yieldq.empty() {
3564+
lock(&sched.lock)
3565+
bg := sched.yieldq.pop()
3566+
unlock(&sched.lock)
3567+
if bg != nil {
3568+
trace := traceAcquire()
3569+
casgstatus(bg, _Gwaiting, _Grunnable)
3570+
if trace.ok() {
3571+
// Match other ready paths for trace visibility.
3572+
trace.GoUnpark(bg, 0)
3573+
traceRelease(trace)
3574+
}
3575+
return bg, false, false
3576+
}
3577+
}
3578+
34483579
// We have nothing to do.
34493580
//
34503581
// If we're in the GC mark phase, can safely scan and blacken objects,
@@ -3509,6 +3640,10 @@ top:
35093640
unlock(&sched.lock)
35103641
return gp, false, false
35113642
}
3643+
if !sched.yieldq.empty() {
3644+
unlock(&sched.lock)
3645+
goto top
3646+
}
35123647
if !mp.spinning && sched.needspinning.Load() == 1 {
35133648
// See "Delicate dance" comment below.
35143649
mp.becomeSpinning()
@@ -7111,6 +7246,19 @@ func (q *gQueue) popList() gList {
71117246
return stack
71127247
}
71137248

7249+
// yield_put is the gopark unlock function for Yield. It enqueues the goroutine
7250+
// onto the global yield queue. Returning true keeps the G parked until another
7251+
// part of the scheduler makes it runnable again. The G remains in _Gwaiting
7252+
// after this returns.
7253+
//
7254+
//go:nosplit
7255+
func yield_put(gp *g, _ unsafe.Pointer) bool {
7256+
lock(&sched.lock)
7257+
sched.yieldq.pushBack(gp)
7258+
unlock(&sched.lock)
7259+
return true
7260+
}
7261+
71147262
// A gList is a list of Gs linked through g.schedlink. A G can only be
71157263
// on one gQueue or gList at a time.
71167264
type gList struct {

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.
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)