Skip to content

Commit 3432c68

Browse files
neildgopherbot
authored andcommitted
runtime: make bubbled timers more consistent with unbubbled
This CL makes two changes to reduce the predictability with which bubbled timers fire. When asynctimerchan=0 (the default), regular timers with an associated channel are only added to a timer heap when some channel operation is blocked on that channel. This allows us to garbage collect unreferenced, unstopped timers. Timers in a synctest bubble, in contrast, are always added to the bubble's timer heap. This CL changes bubbled timers with a channel to be handled the same as unbubbled ones, adding them to the bubble's timer heap only when some channel operation is blocked on the timer's channel. This permits unstopped bubbled timers to be garbage collected, but more importantly it makes all timers past their deadline behave identically, regardless of whether they are in a bubble. This CL also changes timer scheduling to execute bubbled timers immediately when possible rather than adding them to a heap. Timers in a bubble's heap are executed when the bubble is idle. Executing timers immediately avoids creating a predictable order of execution. For #73850 Fixes #73934 Change-Id: If82e441546408f780f6af6fb7f6e416d3160295d Reviewed-on: https://go-review.googlesource.com/c/go/+/678075 Auto-Submit: Damien Neil <dneil@google.com> Reviewed-by: Michael Pratt <mpratt@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 1aa3362 commit 3432c68

File tree

6 files changed

+113
-54
lines changed

6 files changed

+113
-54
lines changed

src/internal/synctest/synctest_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ func TestTimerNondeterminism(t *testing.T) {
226226
const iterations = 1000
227227
var seen1, seen2 bool
228228
for range iterations {
229-
tm1 := time.NewTimer(0)
230-
tm2 := time.NewTimer(0)
229+
tm1 := time.NewTimer(1)
230+
tm2 := time.NewTimer(1)
231231
select {
232232
case <-tm1.C:
233233
seen1 = true
@@ -278,6 +278,57 @@ func TestSleepNondeterminism(t *testing.T) {
278278
})
279279
}
280280

281+
// TestTimerRunsImmediately verifies that a 0-duration timer sends on its channel
282+
// without waiting for the bubble to block.
283+
func TestTimerRunsImmediately(t *testing.T) {
284+
synctest.Run(func() {
285+
start := time.Now()
286+
tm := time.NewTimer(0)
287+
select {
288+
case got := <-tm.C:
289+
if !got.Equal(start) {
290+
t.Errorf("<-tm.C = %v, want %v", got, start)
291+
}
292+
default:
293+
t.Errorf("0-duration timer channel is not readable; want it to be")
294+
}
295+
})
296+
}
297+
298+
// TestTimerRunsLater verifies that reading from a timer's channel receives the
299+
// timer fired, even when that time is in reading from a timer's channel receives the
300+
// time the timer fired, even when that time is in the past.
301+
func TestTimerRanInPast(t *testing.T) {
302+
synctest.Run(func() {
303+
delay := 1 * time.Second
304+
want := time.Now().Add(delay)
305+
tm := time.NewTimer(delay)
306+
time.Sleep(2 * delay)
307+
select {
308+
case got := <-tm.C:
309+
if !got.Equal(want) {
310+
t.Errorf("<-tm.C = %v, want %v", got, want)
311+
}
312+
default:
313+
t.Errorf("0-duration timer channel is not readable; want it to be")
314+
}
315+
})
316+
}
317+
318+
// TestAfterFuncRunsImmediately verifies that a 0-duration AfterFunc is scheduled
319+
// without waiting for the bubble to block.
320+
func TestAfterFuncRunsImmediately(t *testing.T) {
321+
synctest.Run(func() {
322+
var b atomic.Bool
323+
time.AfterFunc(0, func() {
324+
b.Store(true)
325+
})
326+
for !b.Load() {
327+
runtime.Gosched()
328+
}
329+
})
330+
}
331+
281332
func TestChannelFromOutsideBubble(t *testing.T) {
282333
choutside := make(chan struct{})
283334
for _, test := range []struct {

src/runtime/chan.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ func empty(c *hchan) bool {
497497
// c.timer is also immutable (it is set after make(chan) but before any channel operations).
498498
// All timer channels have dataqsiz > 0.
499499
if c.timer != nil {
500-
c.timer.maybeRunChan()
500+
c.timer.maybeRunChan(c)
501501
}
502502
return atomic.Loaduint(&c.qcount) == 0
503503
}
@@ -542,7 +542,7 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
542542
}
543543

544544
if c.timer != nil {
545-
c.timer.maybeRunChan()
545+
c.timer.maybeRunChan(c)
546546
}
547547

548548
// Fast path: check for failed non-blocking operation without acquiring the lock.
@@ -821,7 +821,7 @@ func chanlen(c *hchan) int {
821821
}
822822
async := debug.asynctimerchan.Load() != 0
823823
if c.timer != nil && async {
824-
c.timer.maybeRunChan()
824+
c.timer.maybeRunChan(c)
825825
}
826826
if c.timer != nil && !async {
827827
// timer channels have a buffered implementation

src/runtime/proc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3341,7 +3341,7 @@ top:
33413341
// which may steal timers. It's important that between now
33423342
// and then, nothing blocks, so these numbers remain mostly
33433343
// relevant.
3344-
now, pollUntil, _ := pp.timers.check(0)
3344+
now, pollUntil, _ := pp.timers.check(0, nil)
33453345

33463346
// Try to schedule the trace reader.
33473347
if traceEnabled() || traceShuttingDown() {
@@ -3780,7 +3780,7 @@ func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWo
37803780
// timerpMask tells us whether the P may have timers at all. If it
37813781
// can't, no need to check at all.
37823782
if stealTimersOrRunNextG && timerpMask.read(enum.position()) {
3783-
tnow, w, ran := p2.timers.check(now)
3783+
tnow, w, ran := p2.timers.check(now, nil)
37843784
now = tnow
37853785
if w != 0 && (pollUntil == 0 || w < pollUntil) {
37863786
pollUntil = w

src/runtime/select.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo
185185
}
186186

187187
if cas.c.timer != nil {
188-
cas.c.timer.maybeRunChan()
188+
cas.c.timer.maybeRunChan(cas.c)
189189
}
190190

191191
j := cheaprandn(uint32(norder + 1))

src/runtime/synctest.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ func synctestRun(f func()) {
185185
}
186186
const synctestBaseTime = 946684800000000000 // midnight UTC 2000-01-01
187187
bubble.now = synctestBaseTime
188-
bubble.timers.bubble = bubble
189188
lockInit(&bubble.mu, lockRankSynctest)
190189
lockInit(&bubble.timers.mu, lockRankTimers)
191190

@@ -213,7 +212,7 @@ func synctestRun(f func()) {
213212
// so timer goroutines inherit their child race context from g0.
214213
curg := gp.m.curg
215214
gp.m.curg = nil
216-
gp.bubble.timers.check(gp.bubble.now)
215+
gp.bubble.timers.check(bubble.now, bubble)
217216
gp.m.curg = curg
218217
})
219218
gopark(synctestidle_c, nil, waitReasonSynctestRun, traceBlockSynctest, 0)

src/runtime/time.go

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,6 @@ type timers struct {
157157
// heap[i].when over timers with the timerModified bit set.
158158
// If minWhenModified = 0, it means there are no timerModified timers in the heap.
159159
minWhenModified atomic.Int64
160-
161-
bubble *synctestBubble
162160
}
163161

164162
type timerWhen struct {
@@ -403,7 +401,7 @@ func newTimer(when, period int64, f func(arg any, seq uintptr, delay int64), arg
403401
throw("invalid timer channel: no capacity")
404402
}
405403
}
406-
if gr := getg().bubble; gr != nil {
404+
if bubble := getg().bubble; bubble != nil {
407405
t.isFake = true
408406
}
409407
t.modify(when, period, f, arg, 0)
@@ -485,7 +483,7 @@ func (t *timer) maybeRunAsync() {
485483
// timer ourselves now is fine.)
486484
if now := nanotime(); t.when <= now {
487485
systemstack(func() {
488-
t.unlockAndRun(now) // resets t.when
486+
t.unlockAndRun(now, nil) // resets t.when
489487
})
490488
t.lock()
491489
}
@@ -621,6 +619,29 @@ func (t *timer) modify(when, period int64, f func(arg any, seq uintptr, delay in
621619

622620
add := t.needsAdd()
623621

622+
if add && t.isFake {
623+
// If this is a bubbled timer scheduled to fire immediately,
624+
// run it now rather than waiting for the bubble's timer scheduler.
625+
// This avoids deferring timer execution until after the bubble
626+
// becomes durably blocked.
627+
//
628+
// Don't do this for non-bubbled timers: It isn't necessary,
629+
// and there may be cases where the runtime executes timers with
630+
// the expectation the timer func will not run in the current goroutine.
631+
// Bubbled timers are always created by the time package, and are
632+
// safe to run in the current goroutine.
633+
bubble := getg().bubble
634+
if bubble == nil {
635+
throw("fake timer executing with no bubble")
636+
}
637+
if t.state&timerHeaped == 0 && when <= bubble.now {
638+
systemstack(func() {
639+
t.unlockAndRun(bubble.now, bubble)
640+
})
641+
return pending
642+
}
643+
}
644+
624645
if !async && t.isChan {
625646
// Stop any future sends with stale values.
626647
// See timer.unlockAndRun.
@@ -657,7 +678,7 @@ func (t *timer) modify(when, period int64, f func(arg any, seq uintptr, delay in
657678
// t must be locked.
658679
func (t *timer) needsAdd() bool {
659680
assertLockHeld(&t.mu)
660-
need := t.state&timerHeaped == 0 && t.when > 0 && (!t.isChan || t.isFake || t.blocked > 0)
681+
need := t.state&timerHeaped == 0 && t.when > 0 && (!t.isChan || t.blocked > 0)
661682
if need {
662683
t.trace("needsAdd+")
663684
} else {
@@ -982,7 +1003,7 @@ func (ts *timers) wakeTime() int64 {
9821003
// We pass now in and out to avoid extra calls of nanotime.
9831004
//
9841005
//go:yeswritebarrierrec
985-
func (ts *timers) check(now int64) (rnow, pollUntil int64, ran bool) {
1006+
func (ts *timers) check(now int64, bubble *synctestBubble) (rnow, pollUntil int64, ran bool) {
9861007
ts.trace("check")
9871008
// If it's not yet time for the first timer, or the first adjusted
9881009
// timer, then there is nothing to do.
@@ -1015,7 +1036,7 @@ func (ts *timers) check(now int64) (rnow, pollUntil int64, ran bool) {
10151036
ts.adjust(now, false)
10161037
for len(ts.heap) > 0 {
10171038
// Note that runtimer may temporarily unlock ts.
1018-
if tw := ts.run(now); tw != 0 {
1039+
if tw := ts.run(now, bubble); tw != 0 {
10191040
if tw > 0 {
10201041
pollUntil = tw
10211042
}
@@ -1047,7 +1068,7 @@ func (ts *timers) check(now int64) (rnow, pollUntil int64, ran bool) {
10471068
// If a timer is run, this will temporarily unlock ts.
10481069
//
10491070
//go:systemstack
1050-
func (ts *timers) run(now int64) int64 {
1071+
func (ts *timers) run(now int64, bubble *synctestBubble) int64 {
10511072
ts.trace("run")
10521073
assertLockHeld(&ts.mu)
10531074
Redo:
@@ -1081,7 +1102,7 @@ Redo:
10811102
return t.when
10821103
}
10831104

1084-
t.unlockAndRun(now)
1105+
t.unlockAndRun(now, bubble)
10851106
assertLockHeld(&ts.mu) // t is unlocked now, but not ts
10861107
return 0
10871108
}
@@ -1092,7 +1113,7 @@ Redo:
10921113
// unlockAndRun returns with t unlocked and t.ts (re-)locked.
10931114
//
10941115
//go:systemstack
1095-
func (t *timer) unlockAndRun(now int64) {
1116+
func (t *timer) unlockAndRun(now int64, bubble *synctestBubble) {
10961117
t.trace("unlockAndRun")
10971118
assertLockHeld(&t.mu)
10981119
if t.ts != nil {
@@ -1104,10 +1125,10 @@ func (t *timer) unlockAndRun(now int64) {
11041125
// out from under us while this function executes.
11051126
gp := getg()
11061127
var tsLocal *timers
1107-
if t.ts == nil || t.ts.bubble == nil {
1128+
if bubble == nil {
11081129
tsLocal = &gp.m.p.ptr().timers
11091130
} else {
1110-
tsLocal = &t.ts.bubble.timers
1131+
tsLocal = &bubble.timers
11111132
}
11121133
if tsLocal.raceCtx == 0 {
11131134
tsLocal.raceCtx = racegostart(abi.FuncPCABIInternal((*timers).run) + sys.PCQuantum)
@@ -1160,25 +1181,25 @@ func (t *timer) unlockAndRun(now int64) {
11601181
if gp.racectx != 0 {
11611182
throw("unexpected racectx")
11621183
}
1163-
if ts == nil || ts.bubble == nil {
1184+
if bubble == nil {
11641185
gp.racectx = gp.m.p.ptr().timers.raceCtx
11651186
} else {
1166-
gp.racectx = ts.bubble.timers.raceCtx
1187+
gp.racectx = bubble.timers.raceCtx
11671188
}
11681189
}
11691190

11701191
if ts != nil {
11711192
ts.unlock()
11721193
}
11731194

1174-
if ts != nil && ts.bubble != nil {
1195+
if bubble != nil {
11751196
// Temporarily use the timer's synctest group for the G running this timer.
11761197
gp := getg()
11771198
if gp.bubble != nil {
11781199
throw("unexpected syncgroup set")
11791200
}
1180-
gp.bubble = ts.bubble
1181-
ts.bubble.changegstatus(gp, _Gdead, _Grunning)
1201+
gp.bubble = bubble
1202+
bubble.changegstatus(gp, _Gdead, _Grunning)
11821203
}
11831204

11841205
if !async && t.isChan {
@@ -1222,13 +1243,13 @@ func (t *timer) unlockAndRun(now int64) {
12221243
unlock(&t.sendLock)
12231244
}
12241245

1225-
if ts != nil && ts.bubble != nil {
1246+
if bubble != nil {
12261247
gp := getg()
1227-
ts.bubble.changegstatus(gp, _Grunning, _Gdead)
1248+
bubble.changegstatus(gp, _Grunning, _Gdead)
12281249
if raceenabled {
12291250
// Establish a happens-before between this timer event and
12301251
// the next synctest.Wait call.
1231-
racereleasemergeg(gp, ts.bubble.raceaddr())
1252+
racereleasemergeg(gp, bubble.raceaddr())
12321253
}
12331254
gp.bubble = nil
12341255
}
@@ -1415,24 +1436,10 @@ func badTimer() {
14151436
// maybeRunChan checks whether the timer needs to run
14161437
// to send a value to its associated channel. If so, it does.
14171438
// The timer must not be locked.
1418-
func (t *timer) maybeRunChan() {
1419-
if t.isFake {
1420-
t.lock()
1421-
var timerBubble *synctestBubble
1422-
if t.ts != nil {
1423-
timerBubble = t.ts.bubble
1424-
}
1425-
t.unlock()
1426-
bubble := getg().bubble
1427-
if bubble == nil {
1428-
panic(plainError("synctest timer accessed from outside bubble"))
1429-
}
1430-
if timerBubble != nil && bubble != timerBubble {
1431-
panic(plainError("timer moved between synctest bubbles"))
1432-
}
1433-
// No need to do anything here.
1434-
// synctest.Run will run the timer when it advances its fake clock.
1435-
return
1439+
func (t *timer) maybeRunChan(c *hchan) {
1440+
if t.isFake && getg().bubble != c.bubble {
1441+
// This should have been checked by the caller, but check just in case.
1442+
fatal("synctest timer accessed from outside bubble")
14361443
}
14371444
if t.astate.Load()&timerHeaped != 0 {
14381445
// If the timer is in the heap, the ordinary timer code
@@ -1442,6 +1449,9 @@ func (t *timer) maybeRunChan() {
14421449

14431450
t.lock()
14441451
now := nanotime()
1452+
if t.isFake {
1453+
now = getg().bubble.now
1454+
}
14451455
if t.state&timerHeaped != 0 || t.when == 0 || t.when > now {
14461456
t.trace("maybeRunChan-")
14471457
// Timer in the heap, or not running at all, or not triggered.
@@ -1450,7 +1460,7 @@ func (t *timer) maybeRunChan() {
14501460
}
14511461
t.trace("maybeRunChan+")
14521462
systemstack(func() {
1453-
t.unlockAndRun(now)
1463+
t.unlockAndRun(now, c.bubble)
14541464
})
14551465
}
14561466

@@ -1460,9 +1470,11 @@ func (t *timer) maybeRunChan() {
14601470
// adding it if needed.
14611471
func blockTimerChan(c *hchan) {
14621472
t := c.timer
1463-
if t.isFake {
1464-
return
1473+
if t.isFake && c.bubble != getg().bubble {
1474+
// This should have been checked by the caller, but check just in case.
1475+
fatal("synctest timer accessed from outside bubble")
14651476
}
1477+
14661478
t.lock()
14671479
t.trace("blockTimerChan")
14681480
if !t.isChan {
@@ -1500,9 +1512,6 @@ func blockTimerChan(c *hchan) {
15001512
// blocked on it anymore.
15011513
func unblockTimerChan(c *hchan) {
15021514
t := c.timer
1503-
if t.isFake {
1504-
return
1505-
}
15061515
t.lock()
15071516
t.trace("unblockTimerChan")
15081517
if !t.isChan || t.blocked == 0 {

0 commit comments

Comments
 (0)