Skip to content

Commit 13368ab

Browse files
committed
runtime: clarify which work needs spinning coordination
The overview comments discuss readying goroutines, which is the most common source of work, but timers and idle-priority GC work also require the same synchronization w.r.t. spinning Ms. This CL should have no functional changes. For #43997 Updates #44313 Change-Id: I7910a7f93764dde07c3ed63666277eb832bf8299 Reviewed-on: https://go-review.googlesource.com/c/go/+/307912 Trust: Michael Pratt <[email protected]> Run-TryBot: Michael Pratt <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 800fb11 commit 13368ab

File tree

1 file changed

+72
-34
lines changed

1 file changed

+72
-34
lines changed

src/runtime/proc.go

+72-34
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,64 @@ var modinfo string
5050
// any work to do.
5151
//
5252
// The current approach:
53-
// We unpark an additional thread when we ready a goroutine if (1) there is an
54-
// idle P and there are no "spinning" worker threads. A worker thread is considered
55-
// spinning if it is out of local work and did not find work in global run queue/
56-
// netpoller; the spinning state is denoted in m.spinning and in sched.nmspinning.
57-
// Threads unparked this way are also considered spinning; we don't do goroutine
58-
// handoff so such threads are out of work initially. Spinning threads do some
59-
// spinning looking for work in per-P run queues before parking. If a spinning
53+
//
54+
// This approach applies to three primary sources of potential work: readying a
55+
// goroutine, new/modified-earlier timers, and idle-priority GC. See below for
56+
// additional details.
57+
//
58+
// We unpark an additional thread when we submit work if (this is wakep()):
59+
// 1. There is an idle P, and
60+
// 2. There are no "spinning" worker threads.
61+
//
62+
// A worker thread is considered spinning if it is out of local work and did
63+
// not find work in the global run queue or netpoller; the spinning state is
64+
// denoted in m.spinning and in sched.nmspinning. Threads unparked this way are
65+
// also considered spinning; we don't do goroutine handoff so such threads are
66+
// out of work initially. Spinning threads spin on looking for work in per-P
67+
// run queues and timer heaps or from the GC before parking. If a spinning
6068
// thread finds work it takes itself out of the spinning state and proceeds to
61-
// execution. If it does not find work it takes itself out of the spinning state
62-
// and then parks.
63-
// If there is at least one spinning thread (sched.nmspinning>1), we don't unpark
64-
// new threads when readying goroutines. To compensate for that, if the last spinning
65-
// thread finds work and stops spinning, it must unpark a new spinning thread.
66-
// This approach smooths out unjustified spikes of thread unparking,
67-
// but at the same time guarantees eventual maximal CPU parallelism utilization.
69+
// execution. If it does not find work it takes itself out of the spinning
70+
// state and then parks.
71+
//
72+
// If there is at least one spinning thread (sched.nmspinning>1), we don't
73+
// unpark new threads when submitting work. To compensate for that, if the last
74+
// spinning thread finds work and stops spinning, it must unpark a new spinning
75+
// thread. This approach smooths out unjustified spikes of thread unparking,
76+
// but at the same time guarantees eventual maximal CPU parallelism
77+
// utilization.
78+
//
79+
// The main implementation complication is that we need to be very careful
80+
// during spinning->non-spinning thread transition. This transition can race
81+
// with submission of new work, and either one part or another needs to unpark
82+
// another worker thread. If they both fail to do that, we can end up with
83+
// semi-persistent CPU underutilization.
84+
//
85+
// The general pattern for submission is:
86+
// 1. Submit work to the local run queue, timer heap, or GC state.
87+
// 2. #StoreLoad-style memory barrier.
88+
// 3. Check sched.nmspinning.
6889
//
69-
// The main implementation complication is that we need to be very careful during
70-
// spinning->non-spinning thread transition. This transition can race with submission
71-
// of a new goroutine, and either one part or another needs to unpark another worker
72-
// thread. If they both fail to do that, we can end up with semi-persistent CPU
73-
// underutilization. The general pattern for goroutine readying is: submit a goroutine
74-
// to local work queue, #StoreLoad-style memory barrier, check sched.nmspinning.
75-
// The general pattern for spinning->non-spinning transition is: decrement nmspinning,
76-
// #StoreLoad-style memory barrier, check all per-P work queues for new work.
77-
// Note that all this complexity does not apply to global run queue as we are not
78-
// sloppy about thread unparking when submitting to global queue. Also see comments
79-
// for nmspinning manipulation.
90+
// The general pattern for spinning->non-spinning transition is:
91+
// 1. Decrement nmspinning.
92+
// 2. #StoreLoad-style memory barrier.
93+
// 3. Check all per-P work queues and GC for new work.
94+
//
95+
// Note that all this complexity does not apply to global run queue as we are
96+
// not sloppy about thread unparking when submitting to global queue. Also see
97+
// comments for nmspinning manipulation.
98+
//
99+
// How these different sources of work behave varies, though it doesn't affect
100+
// the synchronization approach:
101+
// * Ready goroutine: this is an obvious source of work; the goroutine is
102+
// immediately ready and must run on some thread eventually.
103+
// * New/modified-earlier timer: The current timer implementation (see time.go)
104+
// uses netpoll in a thread with no work available to wait for the soonest
105+
// timer. If there is no thread waiting, we want a new spinning thread to go
106+
// wait.
107+
// * Idle-priority GC: The GC wakes a stopped idle thread to contribute to
108+
// background GC work (note: currently disabled per golang.org/issue/19112).
109+
// Also see golang.org/issue/44313, as this should be extended to all GC
110+
// workers.
80111

81112
var (
82113
m0 m
@@ -2785,18 +2816,25 @@ stop:
27852816
pidleput(_p_)
27862817
unlock(&sched.lock)
27872818

2788-
// Delicate dance: thread transitions from spinning to non-spinning state,
2789-
// potentially concurrently with submission of new goroutines. We must
2790-
// drop nmspinning first and then check all per-P queues again (with
2791-
// #StoreLoad memory barrier in between). If we do it the other way around,
2792-
// another thread can submit a goroutine after we've checked all run queues
2793-
// but before we drop nmspinning; as a result nobody will unpark a thread
2794-
// to run the goroutine.
2819+
// Delicate dance: thread transitions from spinning to non-spinning
2820+
// state, potentially concurrently with submission of new work. We must
2821+
// drop nmspinning first and then check all sources again (with
2822+
// #StoreLoad memory barrier in between). If we do it the other way
2823+
// around, another thread can submit work after we've checked all
2824+
// sources but before we drop nmspinning; as a result nobody will
2825+
// unpark a thread to run the work.
2826+
//
2827+
// This applies to the following sources of work:
2828+
//
2829+
// * Goroutines added to a per-P run queue.
2830+
// * New/modified-earlier timers on a per-P timer heap.
2831+
// * Idle-priority GC work (barring golang.org/issue/19112).
2832+
//
27952833
// If we discover new work below, we need to restore m.spinning as a signal
27962834
// for resetspinning to unpark a new worker thread (because there can be more
27972835
// than one starving goroutine). However, if after discovering new work
2798-
// we also observe no idle Ps, it is OK to just park the current thread:
2799-
// the system is fully loaded so no spinning threads are required.
2836+
// we also observe no idle Ps it is OK to skip unparking a new worker
2837+
// thread: the system is fully loaded so no spinning threads are required.
28002838
// Also see "Worker thread parking/unparking" comment at the top of the file.
28012839
wasSpinning := _g_.m.spinning
28022840
if _g_.m.spinning {

0 commit comments

Comments
 (0)