Skip to content

Commit 0ec80b8

Browse files
committed
runtime: reduce timer latency
Change the scheduler to consider P's not marked for preemption as potential targets for timer stealing by spinning P's. Ignoring timers on P's not marked for preemption, as the scheduler did previously, has the downside that timers on that P must wait for its current G to complete or get preempted. But that can take as long as 10ms. In addition, this choice is only made when a spinning P is available and in timer-bound applications it may result in the spinning P stopping instead of performing available work, reducing parallelism. In CL 214185 we avoided taking the timer lock of a P with no ready timers, which reduces the chances of timer lock contention. Fixes golang#38860 Change-Id: If52680509b0f3b66dbd1d0c13fa574bd2d0bbd57
1 parent b5f7ff4 commit 0ec80b8

File tree

2 files changed

+87
-29
lines changed

2 files changed

+87
-29
lines changed

src/runtime/proc.go

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2213,14 +2213,18 @@ top:
22132213
return gp, false
22142214
}
22152215

2216-
// Consider stealing timers from p2.
2217-
// This call to checkTimers is the only place where
2218-
// we hold a lock on a different P's timers.
2219-
// Lock contention can be a problem here, so avoid
2220-
// grabbing the lock if p2 is running and not marked
2221-
// for preemption. If p2 is running and not being
2222-
// preempted we assume it will handle its own timers.
2223-
if i > 2 && shouldStealTimers(p2) {
2216+
// Steal timers from p2. This call to checkTimers is the only place
2217+
// where we might hold a lock on a different P's timers. We do this
2218+
// when i == 2 because that is the pass before runqsteal considers
2219+
// G's in the runnext slot of p2. Stealing from the other P's
2220+
// runnext should be the last resort, so if there are timers to
2221+
// steal, do that first.
2222+
//
2223+
// We only check timers on one of the stealing iterations because
2224+
// the time stored in now doesn't change in this loop and checking
2225+
// the timers for each P more than once with the same value of now
2226+
// is probably a waste of time.
2227+
if i == 2 {
22242228
tnow, w, ran := checkTimers(p2, now)
22252229
now = tnow
22262230
if w != 0 && (pollUntil == 0 || w < pollUntil) {
@@ -2447,6 +2451,10 @@ func wakeNetPoller(when int64) {
24472451
if pollerPollUntil == 0 || pollerPollUntil > when {
24482452
netpollBreak()
24492453
}
2454+
} else {
2455+
// There are no threads in the network poller, try to get
2456+
// one there so it can handle new timers.
2457+
wakep()
24502458
}
24512459
}
24522460

@@ -2728,25 +2736,6 @@ func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) {
27282736
return rnow, pollUntil, ran
27292737
}
27302738

2731-
// shouldStealTimers reports whether we should try stealing the timers from p2.
2732-
// We don't steal timers from a running P that is not marked for preemption,
2733-
// on the assumption that it will run its own timers. This reduces
2734-
// contention on the timers lock.
2735-
func shouldStealTimers(p2 *p) bool {
2736-
if p2.status != _Prunning {
2737-
return true
2738-
}
2739-
mp := p2.m.ptr()
2740-
if mp == nil || mp.locks > 0 {
2741-
return false
2742-
}
2743-
gp := mp.curg
2744-
if gp == nil || gp.atomicstatus != _Grunning || !gp.preempt {
2745-
return false
2746-
}
2747-
return true
2748-
}
2749-
27502739
func parkunlock_c(gp *g, lock unsafe.Pointer) bool {
27512740
unlock((*mutex)(lock))
27522741
return true
@@ -4628,8 +4617,7 @@ func sysmon() {
46284617
}
46294618
}
46304619
if next < now {
4631-
// There are timers that should have already run,
4632-
// perhaps because there is an unpreemptible P.
4620+
// There are timers that should have already run.
46334621
// Try to start an M to run them.
46344622
startm(nil, false)
46354623
}

src/time/sleep_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
package time_test
66

77
import (
8+
"context"
89
"errors"
910
"fmt"
1011
"runtime"
12+
"runtime/trace"
1113
"strings"
1214
"sync"
1315
"sync/atomic"
@@ -501,3 +503,71 @@ func TestZeroTimerStopPanics(t *testing.T) {
501503
var tr Timer
502504
tr.Stop()
503505
}
506+
507+
func TestTimerParallelism(t *testing.T) {
508+
gmp := runtime.GOMAXPROCS(0)
509+
if gmp < 2 || runtime.NumCPU() < gmp {
510+
t.Skip("skipping with GOMAXPROCS < 2 or NumCPU < GOMAXPROCS")
511+
}
512+
513+
doWork := func(dur Duration) {
514+
start := Now()
515+
for Since(start) < dur {
516+
}
517+
}
518+
519+
var wg sync.WaitGroup
520+
521+
// Warm up the scheduler's thread pool. Without this step the time to start
522+
// new threads as the parallelism increases on high CPU count machines can
523+
// cause false test failures.
524+
var count int32
525+
wg.Add(gmp)
526+
for i := 0; i < gmp; i++ {
527+
go func() {
528+
defer wg.Done()
529+
atomic.AddInt32(&count, 1)
530+
for atomic.LoadInt32(&count) < int32(gmp) {
531+
// spin until all threads started
532+
}
533+
// spin a bit more to ensure they are all running on separate CPUs.
534+
trace.Log(context.Background(), "event", "spinning hot")
535+
doWork(10 * Millisecond)
536+
}()
537+
}
538+
wg.Wait()
539+
540+
// Let most of the threads stop. Sleeping for a bit also puts some space
541+
// between the warm up and main test when looking at execution trace data
542+
// for this test.
543+
Sleep(5 * Millisecond)
544+
545+
const (
546+
delay = Millisecond
547+
grace = 4 * Millisecond
548+
)
549+
550+
for k := 0; k < 10; k++ {
551+
k := k
552+
553+
wg.Add(gmp - 1)
554+
for i := 0; i < gmp-1; i++ {
555+
name := fmt.Sprintf("timerAfter-%d-%d", k, i)
556+
557+
trace.Log(context.Background(), "event", "AfterFunc")
558+
expectedWakeup := Now().Add(delay)
559+
AfterFunc(delay, func() {
560+
if late := Since(expectedWakeup); late > grace {
561+
trace.Log(context.Background(), "event", "late wakeup")
562+
t.Errorf("%s: wakeup late by %v", name, late)
563+
}
564+
doWork(10 * Millisecond)
565+
wg.Done()
566+
})
567+
}
568+
569+
doWork(50 * Millisecond)
570+
}
571+
572+
wg.Wait()
573+
}

0 commit comments

Comments
 (0)