@@ -73,7 +73,7 @@ var modinfo string
73
73
// If there is at least one spinning thread (sched.nmspinning>1), we don't
74
74
// unpark new threads when submitting work. To compensate for that, if the last
75
75
// spinning thread finds work and stops spinning, it must unpark a new spinning
76
- // thread. This approach smooths out unjustified spikes of thread unparking,
76
+ // thread. This approach smooths out unjustified spikes of thread unparking,
77
77
// but at the same time guarantees eventual maximal CPU parallelism
78
78
// utilization.
79
79
//
@@ -827,6 +827,12 @@ func mcommoninit(mp *m, id int64) {
827
827
}
828
828
}
829
829
830
+ func (mp * m ) becomeSpinning () {
831
+ mp .spinning = true
832
+ sched .nmspinning .Add (1 )
833
+ sched .needspinning .Store (0 )
834
+ }
835
+
830
836
var fastrandseed uintptr
831
837
832
838
func fastrandinit () {
@@ -2242,8 +2248,8 @@ func mspinning() {
2242
2248
// Schedules some M to run the p (creates an M if necessary).
2243
2249
// If p==nil, tries to get an idle P, if no idle P's does nothing.
2244
2250
// May run with m.p==nil, so write barriers are not allowed.
2245
- // If spinning is set, the caller has incremented nmspinning and startm will
2246
- // either decrement nmspinning or set m.spinning in the newly started M.
2251
+ // If spinning is set, the caller has incremented nmspinning and must provide a
2252
+ // P. startm will set m.spinning in the newly started M.
2247
2253
//
2248
2254
// Callers passing a non-nil P must call from a non-preemptible context. See
2249
2255
// comment on acquirem below.
@@ -2271,16 +2277,15 @@ func startm(pp *p, spinning bool) {
2271
2277
mp := acquirem ()
2272
2278
lock (& sched .lock )
2273
2279
if pp == nil {
2280
+ if spinning {
2281
+ // TODO(prattmic): All remaining calls to this function
2282
+ // with _p_ == nil could be cleaned up to find a P
2283
+ // before calling startm.
2284
+ throw ("startm: P required for spinning=true" )
2285
+ }
2274
2286
pp , _ = pidleget (0 )
2275
2287
if pp == nil {
2276
2288
unlock (& sched .lock )
2277
- if spinning {
2278
- // The caller incremented nmspinning, but there are no idle Ps,
2279
- // so it's okay to just undo the increment and give up.
2280
- if sched .nmspinning .Add (- 1 ) < 0 {
2281
- throw ("startm: negative nmspinning" )
2282
- }
2283
- }
2284
2289
releasem (mp )
2285
2290
return
2286
2291
}
@@ -2358,6 +2363,7 @@ func handoffp(pp *p) {
2358
2363
// no local work, check that there are no spinning/idle M's,
2359
2364
// otherwise our help is not required
2360
2365
if sched .nmspinning .Load ()+ sched .npidle .Load () == 0 && sched .nmspinning .CompareAndSwap (0 , 1 ) { // TODO: fast atomic
2366
+ sched .needspinning .Store (0 )
2361
2367
startm (pp , true )
2362
2368
return
2363
2369
}
@@ -2404,15 +2410,41 @@ func handoffp(pp *p) {
2404
2410
2405
2411
// Tries to add one more P to execute G's.
2406
2412
// Called when a G is made runnable (newproc, ready).
2413
+ // Must be called with a P.
2407
2414
func wakep () {
2408
- if sched .npidle .Load () == 0 {
2415
+ // Be conservative about spinning threads, only start one if none exist
2416
+ // already.
2417
+ if sched .nmspinning .Load () != 0 || ! sched .nmspinning .CompareAndSwap (0 , 1 ) {
2409
2418
return
2410
2419
}
2411
- // be conservative about spinning threads
2412
- if sched .nmspinning .Load () != 0 || ! sched .nmspinning .CompareAndSwap (0 , 1 ) {
2420
+
2421
+ // Disable preemption until ownership of pp transfers to the next M in
2422
+ // startm. Otherwise preemption here would leave pp stuck waiting to
2423
+ // enter _Pgcstop.
2424
+ //
2425
+ // See preemption comment on acquirem in startm for more details.
2426
+ mp := acquirem ()
2427
+
2428
+ var pp * p
2429
+ lock (& sched .lock )
2430
+ pp , _ = pidlegetSpinning (0 )
2431
+ if pp == nil {
2432
+ if sched .nmspinning .Add (- 1 ) < 0 {
2433
+ throw ("wakep: negative nmspinning" )
2434
+ }
2435
+ unlock (& sched .lock )
2436
+ releasem (mp )
2413
2437
return
2414
2438
}
2415
- startm (nil , true )
2439
+ // Since we always have a P, the race in the "No M is available"
2440
+ // comment in startm doesn't apply during the small window between the
2441
+ // unlock here and lock in startm. A checkdead in between will always
2442
+ // see at least one running M (ours).
2443
+ unlock (& sched .lock )
2444
+
2445
+ startm (pp , true )
2446
+
2447
+ releasem (mp )
2416
2448
}
2417
2449
2418
2450
// Stops execution of the current m that is locked to a g until the g is runnable again.
@@ -2646,8 +2678,7 @@ top:
2646
2678
// GOMAXPROCS>>1 but the program parallelism is low.
2647
2679
if mp .spinning || 2 * sched .nmspinning .Load () < gomaxprocs - sched .npidle .Load () {
2648
2680
if ! mp .spinning {
2649
- mp .spinning = true
2650
- sched .nmspinning .Add (1 )
2681
+ mp .becomeSpinning ()
2651
2682
}
2652
2683
2653
2684
gp , inheritTime , tnow , w , newWork := stealWork (now )
@@ -2723,6 +2754,12 @@ top:
2723
2754
unlock (& sched .lock )
2724
2755
return gp , false , false
2725
2756
}
2757
+ if ! mp .spinning && sched .needspinning .Load () == 1 {
2758
+ // See "Delicate dance" comment below.
2759
+ mp .becomeSpinning ()
2760
+ unlock (& sched .lock )
2761
+ goto top
2762
+ }
2726
2763
if releasep () != pp {
2727
2764
throw ("findrunnable: wrong p" )
2728
2765
}
@@ -2743,12 +2780,28 @@ top:
2743
2780
// * New/modified-earlier timers on a per-P timer heap.
2744
2781
// * Idle-priority GC work (barring golang.org/issue/19112).
2745
2782
//
2746
- // If we discover new work below, we need to restore m.spinning as a signal
2747
- // for resetspinning to unpark a new worker thread (because there can be more
2748
- // than one starving goroutine). However, if after discovering new work
2749
- // we also observe no idle Ps it is OK to skip unparking a new worker
2750
- // thread: the system is fully loaded so no spinning threads are required.
2751
- // Also see "Worker thread parking/unparking" comment at the top of the file.
2783
+ // If we discover new work below, we need to restore m.spinning as a
2784
+ // signal for resetspinning to unpark a new worker thread (because
2785
+ // there can be more than one starving goroutine).
2786
+ //
2787
+ // However, if after discovering new work we also observe no idle Ps
2788
+ // (either here or in resetspinning), we have a problem. We may be
2789
+ // racing with a non-spinning M in the block above, having found no
2790
+ // work and preparing to release its P and park. Allowing that P to go
2791
+ // idle will result in loss of work conservation (idle P while there is
2792
+ // runnable work). This could result in complete deadlock in the
2793
+ // unlikely event that we discover new work (from netpoll) right as we
2794
+ // are racing with _all_ other Ps going idle.
2795
+ //
2796
+ // We use sched.needspinning to synchronize with non-spinning Ms going
2797
+ // idle. If needspinning is set when they are about to drop their P,
2798
+ // they abort the drop and instead become a new spinning M on our
2799
+ // behalf. If we are not racing and the system is truly fully loaded
2800
+ // then no spinning threads are required, and the next thread to
2801
+ // naturally become spinning will clear the flag.
2802
+ //
2803
+ // Also see "Worker thread parking/unparking" comment at the top of the
2804
+ // file.
2752
2805
wasSpinning := mp .spinning
2753
2806
if mp .spinning {
2754
2807
mp .spinning = false
@@ -2758,25 +2811,26 @@ top:
2758
2811
2759
2812
// Note the for correctness, only the last M transitioning from
2760
2813
// spinning to non-spinning must perform these rechecks to
2761
- // ensure no missed work. We are performing it on every M that
2762
- // transitions as a conservative change to monitor effects on
2763
- // latency. See golang.org/issue/43997.
2814
+ // ensure no missed work. However, the runtime has some cases
2815
+ // of transient increments of nmspinning that are decremented
2816
+ // without going through this path, so we must be conservative
2817
+ // and perform the check on all spinning Ms.
2818
+ //
2819
+ // See https://go.dev/issue/43997.
2764
2820
2765
2821
// Check all runqueues once again.
2766
2822
pp := checkRunqsNoP (allpSnapshot , idlepMaskSnapshot )
2767
2823
if pp != nil {
2768
2824
acquirep (pp )
2769
- mp .spinning = true
2770
- sched .nmspinning .Add (1 )
2825
+ mp .becomeSpinning ()
2771
2826
goto top
2772
2827
}
2773
2828
2774
2829
// Check for idle-priority GC work again.
2775
2830
pp , gp := checkIdleGCNoP ()
2776
2831
if pp != nil {
2777
2832
acquirep (pp )
2778
- mp .spinning = true
2779
- sched .nmspinning .Add (1 )
2833
+ mp .becomeSpinning ()
2780
2834
2781
2835
// Run the idle worker.
2782
2836
pp .gcMarkWorkerMode = gcMarkWorkerIdleMode
@@ -2844,8 +2898,7 @@ top:
2844
2898
return gp , false , false
2845
2899
}
2846
2900
if wasSpinning {
2847
- mp .spinning = true
2848
- sched .nmspinning .Add (1 )
2901
+ mp .becomeSpinning ()
2849
2902
}
2850
2903
goto top
2851
2904
}
@@ -2964,17 +3017,18 @@ func checkRunqsNoP(allpSnapshot []*p, idlepMaskSnapshot pMask) *p {
2964
3017
for id , p2 := range allpSnapshot {
2965
3018
if ! idlepMaskSnapshot .read (uint32 (id )) && ! runqempty (p2 ) {
2966
3019
lock (& sched .lock )
2967
- pp , _ := pidleget (0 )
2968
- unlock (& sched .lock )
2969
- if pp != nil {
2970
- return pp
3020
+ pp , _ := pidlegetSpinning (0 )
3021
+ if pp == nil {
3022
+ // Can't get a P, don't bother checking remaining Ps.
3023
+ unlock (& sched .lock )
3024
+ return nil
2971
3025
}
2972
-
2973
- // Can't get a P, don't bother checking remaining Ps.
2974
- break
3026
+ unlock (& sched .lock )
3027
+ return pp
2975
3028
}
2976
3029
}
2977
3030
3031
+ // No work available.
2978
3032
return nil
2979
3033
}
2980
3034
@@ -3030,7 +3084,7 @@ func checkIdleGCNoP() (*p, *g) {
3030
3084
// the assumption in gcControllerState.findRunnableGCWorker that an
3031
3085
// empty gcBgMarkWorkerPool is only possible if gcMarkDone is running.
3032
3086
lock (& sched .lock )
3033
- pp , now := pidleget (0 )
3087
+ pp , now := pidlegetSpinning (0 )
3034
3088
if pp == nil {
3035
3089
unlock (& sched .lock )
3036
3090
return nil , nil
@@ -3130,8 +3184,20 @@ func injectglist(glist *gList) {
3130
3184
* glist = gList {}
3131
3185
3132
3186
startIdle := func (n int ) {
3133
- for ; n != 0 && sched .npidle .Load () != 0 ; n -- {
3134
- startm (nil , false )
3187
+ for i := 0 ; i < n ; i ++ {
3188
+ mp := acquirem () // See comment in startm.
3189
+ lock (& sched .lock )
3190
+
3191
+ pp , _ := pidlegetSpinning (0 )
3192
+ if pp == nil {
3193
+ unlock (& sched .lock )
3194
+ releasem (mp )
3195
+ break
3196
+ }
3197
+
3198
+ unlock (& sched .lock )
3199
+ startm (pp , false )
3200
+ releasem (mp )
3135
3201
}
3136
3202
}
3137
3203
@@ -5406,7 +5472,7 @@ func schedtrace(detailed bool) {
5406
5472
}
5407
5473
5408
5474
lock (& sched .lock )
5409
- print ("SCHED " , (now - starttime )/ 1e6 , "ms: gomaxprocs=" , gomaxprocs , " idleprocs=" , sched .npidle .Load (), " threads=" , mcount (), " spinningthreads=" , sched .nmspinning .Load (), " idlethreads=" , sched .nmidle , " runqueue=" , sched .runqsize )
5475
+ print ("SCHED " , (now - starttime )/ 1e6 , "ms: gomaxprocs=" , gomaxprocs , " idleprocs=" , sched .npidle .Load (), " threads=" , mcount (), " spinningthreads=" , sched .nmspinning .Load (), " needspinning=" , sched . needspinning . Load (), " idlethreads=" , sched .nmidle , " runqueue=" , sched .runqsize )
5410
5476
if detailed {
5411
5477
print (" gcwaiting=" , sched .gcwaiting .Load (), " nmidlelocked=" , sched .nmidlelocked , " stopwait=" , sched .stopwait , " sysmonwait=" , sched .sysmonwait .Load (), "\n " )
5412
5478
}
@@ -5742,6 +5808,31 @@ func pidleget(now int64) (*p, int64) {
5742
5808
return pp , now
5743
5809
}
5744
5810
5811
+ // pidlegetSpinning tries to get a p from the _Pidle list, acquiring ownership.
5812
+ // This is called by spinning Ms (or callers than need a spinning M) that have
5813
+ // found work. If no P is available, this must synchronized with non-spinning
5814
+ // Ms that may be preparing to drop their P without discovering this work.
5815
+ //
5816
+ // sched.lock must be held.
5817
+ //
5818
+ // May run during STW, so write barriers are not allowed.
5819
+ //
5820
+ //go:nowritebarrierrec
5821
+ func pidlegetSpinning (now int64 ) (* p , int64 ) {
5822
+ assertLockHeld (& sched .lock )
5823
+
5824
+ pp , now := pidleget (now )
5825
+ if pp == nil {
5826
+ // See "Delicate dance" comment in findrunnable. We found work
5827
+ // that we cannot take, we must synchronize with non-spinning
5828
+ // Ms that may be preparing to drop their P.
5829
+ sched .needspinning .Store (1 )
5830
+ return nil , now
5831
+ }
5832
+
5833
+ return pp , now
5834
+ }
5835
+
5745
5836
// runqempty reports whether pp has no Gs on its local run queue.
5746
5837
// It never returns true spuriously.
5747
5838
func runqempty (pp * p ) bool {
0 commit comments