Skip to content

Commit 508bb17

Browse files
committed
time: garbage collect unstopped Tickers and Timers
From the beginning of Go, the time package has had a gotcha: if you use a select on <-time.After(1*time.Minute), even if the select finishes immediately because some other case is ready, the underlying timer from time.After keeps running until the minute is over. This pins the timer in the timer heap, which keeps it from being garbage collected and in extreme cases also slows down timer operations. The lack of garbage collection is the more important problem. The docs for After warn against this scenario and suggest using NewTimer with a call to Stop after the select instead, purely to work around this garbage collection problem. Oddly, the docs for NewTimer and NewTicker do not mention this problem, but they have the same issue: they cannot be collected until either they are Stopped or, in the case of Timer, the timer expires. (Tickers repeat, so they never expire.) People have built up a shared knowledge that timers and tickers need to defer t.Stop even though the docs do not mention this (it is somewhat implied by the After docs). This CL fixes the garbage collection problem, so that a timer that is unreferenced can be GC'ed immediately, even if it is still running. The approach is to only insert the timer into the heap when some channel operation is blocked on it; the last channel operation to stop using the timer takes it back out of the heap. When a timer's channel is no longer referenced, there are no channel operations blocked on it, so it's not in the heap, so it can be GC'ed immediately. This CL adds an undocumented GODEBUG asynctimerchan=1 that will disable the change. The documentation happens in the CL 568341. Fixes #8898. Fixes #61542. Change-Id: Ieb303b6de1fb3527d3256135151a9e983f3c27e6 Reviewed-on: https://go-review.googlesource.com/c/go/+/512355 Reviewed-by: Austin Clements <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Russ Cox <[email protected]>
1 parent 74a0e31 commit 508bb17

16 files changed

+699
-73
lines changed

doc/godebug.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ and the [go command documentation](/cmd/go#hdr-Build_and_test_caching).
128128

129129
### Go 1.23
130130

131+
TODO: `asynctimerchan` setting.
132+
131133
Go 1.23 changed the mode bits reported by [`os.Lstat`](/pkg/os#Lstat) and [`os.Stat`](/pkg/os#Stat)
132134
for reparse points, which can be controlled with the `winsymlink` setting.
133135
As of Go 1.23 (`winsymlink=1`), mount points no longer have [`os.ModeSymlink`](/pkg/os#ModeSymlink)

src/internal/godebugs/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Info struct {
2525
// Note: After adding entries to this table, update the list in doc/godebug.md as well.
2626
// (Otherwise the test in this package will fail.)
2727
var All = []Info{
28+
{Name: "asynctimerchan", Package: "time", Opaque: true},
2829
{Name: "execerrdot", Package: "os/exec"},
2930
{Name: "gocachehash", Package: "cmd/go"},
3031
{Name: "gocachetest", Package: "cmd/go"},

src/runtime/chan.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type hchan struct {
3636
buf unsafe.Pointer // points to an array of dataqsiz elements
3737
elemsize uint16
3838
closed uint32
39+
timer *timer // timer feeding this chan
3940
elemtype *_type // element type
4041
sendx uint // send index
4142
recvx uint // receive index
@@ -426,12 +427,19 @@ func closechan(c *hchan) {
426427
}
427428

428429
// empty reports whether a read from c would block (that is, the channel is
429-
// empty). It uses a single atomic read of mutable state.
430+
// empty). It is atomically correct and sequentially consistent at the moment
431+
// it returns, but since the channel is unlocked, the channel may become
432+
// non-empty immediately afterward.
430433
func empty(c *hchan) bool {
431434
// c.dataqsiz is immutable.
432435
if c.dataqsiz == 0 {
433436
return atomic.Loadp(unsafe.Pointer(&c.sendq.first)) == nil
434437
}
438+
// c.timer is also immutable (it is set after make(chan) but before any channel operations).
439+
// All timer channels have dataqsiz > 0.
440+
if c.timer != nil {
441+
c.timer.maybeRunChan()
442+
}
435443
return atomic.Loaduint(&c.qcount) == 0
436444
}
437445

@@ -470,6 +478,10 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
470478
throw("unreachable")
471479
}
472480

481+
if c.timer != nil {
482+
c.timer.maybeRunChan()
483+
}
484+
473485
// Fast path: check for failed non-blocking operation without acquiring the lock.
474486
if !block && empty(c) {
475487
// After observing that the channel is not ready for receiving, we observe whether the
@@ -570,11 +582,16 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
570582
mysg.elem = ep
571583
mysg.waitlink = nil
572584
gp.waiting = mysg
585+
573586
mysg.g = gp
574587
mysg.isSelect = false
575588
mysg.c = c
576589
gp.param = nil
577590
c.recvq.enqueue(mysg)
591+
if c.timer != nil {
592+
blockTimerChan(c)
593+
}
594+
578595
// Signal to anyone trying to shrink our stack that we're about
579596
// to park on a channel. The window between when this G's status
580597
// changes and when we set gp.activeStackChans is not safe for
@@ -586,6 +603,9 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
586603
if mysg != gp.waiting {
587604
throw("G waiting list is corrupted")
588605
}
606+
if c.timer != nil {
607+
unblockTimerChan(c)
608+
}
589609
gp.waiting = nil
590610
gp.activeStackChans = false
591611
if mysg.releasetime > 0 {
@@ -728,6 +748,9 @@ func chanlen(c *hchan) int {
728748
if c == nil {
729749
return 0
730750
}
751+
if c.timer != nil {
752+
c.timer.maybeRunChan()
753+
}
731754
return int(c.qcount)
732755
}
733756

src/runtime/lockrank.go

Lines changed: 24 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/runtime/mgcscavenge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func (s *scavengerState) init() {
361361
s.g = getg()
362362

363363
s.timer = new(timer)
364-
f := func(s any, _ uintptr) {
364+
f := func(s any, _ uintptr, _ int64) {
365365
s.(*scavengerState).wake()
366366
}
367367
s.timer.init(f, s)

src/runtime/mklockrank.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,17 @@ assistQueue,
7272
< SCHED
7373
# Below SCHED is the scheduler implementation.
7474
< allocmR,
75-
execR
76-
< sched;
75+
execR;
76+
allocmR, execR, hchan < sched;
7777
sched < allg, allp;
78-
hchan, pollDesc, wakeableSleep < timers;
79-
timers < timer < netpollInit;
8078
8179
# Channels
8280
NONE < notifyList;
8381
hchan, notifyList < sudog;
8482
83+
hchan, pollDesc, wakeableSleep < timers;
84+
timers < timer < netpollInit;
85+
8586
# Semaphores
8687
NONE < root;
8788

src/runtime/netpoll.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -658,15 +658,15 @@ func netpolldeadlineimpl(pd *pollDesc, seq uintptr, read, write bool) {
658658
netpollAdjustWaiters(delta)
659659
}
660660

661-
func netpollDeadline(arg any, seq uintptr) {
661+
func netpollDeadline(arg any, seq uintptr, delta int64) {
662662
netpolldeadlineimpl(arg.(*pollDesc), seq, true, true)
663663
}
664664

665-
func netpollReadDeadline(arg any, seq uintptr) {
665+
func netpollReadDeadline(arg any, seq uintptr, delta int64) {
666666
netpolldeadlineimpl(arg.(*pollDesc), seq, true, false)
667667
}
668668

669-
func netpollWriteDeadline(arg any, seq uintptr) {
669+
func netpollWriteDeadline(arg any, seq uintptr, delta int64) {
670670
netpolldeadlineimpl(arg.(*pollDesc), seq, false, true)
671671
}
672672

src/runtime/runtime1.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,25 @@ var debug struct {
339339
sbrk int32
340340

341341
panicnil atomic.Int32
342+
343+
// asynctimerchan controls whether timer channels
344+
// behave asynchronously (as in Go 1.22 and earlier)
345+
// instead of their Go 1.23+ synchronous behavior.
346+
// The value can change at any time (in response to os.Setenv("GODEBUG"))
347+
// and affects all extant timer channels immediately.
348+
// Programs wouldn't normally change over an execution,
349+
// but allowing it is convenient for testing and for programs
350+
// that do an os.Setenv in main.init or main.main.
351+
asynctimerchan atomic.Int32
342352
}
343353

344354
var dbgvars = []*dbgVar{
355+
{name: "adaptivestackstart", value: &debug.adaptivestackstart},
345356
{name: "allocfreetrace", value: &debug.allocfreetrace},
346-
{name: "clobberfree", value: &debug.clobberfree},
357+
{name: "asyncpreemptoff", value: &debug.asyncpreemptoff},
358+
{name: "asynctimerchan", atomic: &debug.asynctimerchan},
347359
{name: "cgocheck", value: &debug.cgocheck},
360+
{name: "clobberfree", value: &debug.clobberfree},
348361
{name: "disablethp", value: &debug.disablethp},
349362
{name: "dontfreezetheworld", value: &debug.dontfreezetheworld},
350363
{name: "efence", value: &debug.efence},
@@ -353,21 +366,19 @@ var dbgvars = []*dbgVar{
353366
{name: "gcshrinkstackoff", value: &debug.gcshrinkstackoff},
354367
{name: "gcstoptheworld", value: &debug.gcstoptheworld},
355368
{name: "gctrace", value: &debug.gctrace},
369+
{name: "harddecommit", value: &debug.harddecommit},
370+
{name: "inittrace", value: &debug.inittrace},
356371
{name: "invalidptr", value: &debug.invalidptr},
357372
{name: "madvdontneed", value: &debug.madvdontneed},
373+
{name: "panicnil", atomic: &debug.panicnil},
358374
{name: "runtimecontentionstacks", atomic: &debug.runtimeContentionStacks},
359375
{name: "sbrk", value: &debug.sbrk},
360376
{name: "scavtrace", value: &debug.scavtrace},
361377
{name: "scheddetail", value: &debug.scheddetail},
362378
{name: "schedtrace", value: &debug.schedtrace},
379+
{name: "traceadvanceperiod", value: &debug.traceadvanceperiod},
363380
{name: "tracebackancestors", value: &debug.tracebackancestors},
364-
{name: "asyncpreemptoff", value: &debug.asyncpreemptoff},
365-
{name: "inittrace", value: &debug.inittrace},
366-
{name: "harddecommit", value: &debug.harddecommit},
367-
{name: "adaptivestackstart", value: &debug.adaptivestackstart},
368381
{name: "tracefpunwindoff", value: &debug.tracefpunwindoff},
369-
{name: "panicnil", atomic: &debug.panicnil},
370-
{name: "traceadvanceperiod", value: &debug.traceadvanceperiod},
371382
}
372383

373384
func parsedebugvars() {

src/runtime/select.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo
173173
continue
174174
}
175175

176+
if cas.c.timer != nil {
177+
cas.c.timer.maybeRunChan()
178+
}
179+
176180
j := cheaprandn(uint32(norder + 1))
177181
pollorder[norder] = pollorder[j]
178182
pollorder[j] = uint16(i)
@@ -315,6 +319,10 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo
315319
} else {
316320
c.recvq.enqueue(sg)
317321
}
322+
323+
if c.timer != nil {
324+
blockTimerChan(c)
325+
}
318326
}
319327

320328
// wait for someone to wake us up
@@ -351,6 +359,9 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo
351359

352360
for _, casei := range lockorder {
353361
k = &scases[casei]
362+
if k.c.timer != nil {
363+
unblockTimerChan(k.c)
364+
}
354365
if sg == sglist {
355366
// sg has already been dequeued by the G that woke us up.
356367
casi = int(casei)

0 commit comments

Comments
 (0)