Files
tinygo/src/runtime/time.go
T
Ron Evans e569dcfe38 runtime: fix ticker not stopping when Stop races with its callback (#5487)
* runtime: fix ticker not stopping when Stop races with its callback

On the threads and cores schedulers, timer callbacks run concurrently
with user goroutines.

If Stop (or Reset) was called in the window after the node was popped
but before its callback re-added it, removeTimer would not find the
timer in the queue, so it would be re-added to timer anyway.

Track timers whose callback is currently running in a firing list, and
have removeTimer mark a firing timer as stopped so its callback does not
re-add it.

Also make the timers test drain robust: allow a possible in-flight tick
delivered concurrently with Stop to settle before draining the channel.

Signed-off-by: deadprogram <ron@hybridgroup.com>

* runtime: fix Stop/Reset semantics when racing a firing timer callback

Address review feedback on the ticker Stop-race fix. On the threads and
cores schedulers a timer callback runs concurrently with user goroutines,
which left several problems:

- removeTimer reported a firing timer as successfully removed, so
  Stop/Reset could return true even though the callback had already
  started (wrong semantics, notably for AfterFunc). firingTimerStop now
  returns a bool and removeTimer no longer hands the still-firing node
  back to resetTimer.

- The periodic advance (when += period) ran in timerCallback outside the
  scheduler timer lock. Move it into each scheduler's reAddTimer, under
  the lock and after the stopped check, so a concurrent Reset can't have
  its freshly-queued deadline corrupted.

- resetTimer now sets when/period after removeTimer for the same reason.

Add testdata/timer_stop_reset_race.go and TestTimerStopResetRace, which
reproduce the stop-while-firing and reset-while-firing races via the
runtime timer linkname hooks.

Signed-off-by: deadprogram <ron@hybridgroup.com>

* runtime: fix timer Stop/Reset race with firing periodic timers

Signed-off-by: deadprogram <ron@hybridgroup.com>

---------

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-07-06 20:14:53 +02:00

133 lines
3.9 KiB
Go

package runtime
import "unsafe"
// This is the timer that's used internally inside the runtime.
type timer struct {
// When to call the timer, and the interval for the ticker.
when int64
period int64
// Callback from the time package.
f func(arg any, seq uintptr, delta int64)
arg any
}
func (tim *timer) callCallback(delta int64) {
tim.f(tim.arg, 0, delta)
}
// This is the struct used internally in the runtime. The first two fields are
// the same as time.Timer and time.Ticker so it can be used as-is in the time
// package.
type timeTimer struct {
c unsafe.Pointer // <-chan time.Time
init bool
timer
}
//go:linkname newTimer time.newTimer
func newTimer(when, period int64, f func(arg any, seq uintptr, delta int64), arg any, c unsafe.Pointer) *timeTimer {
tim := &timeTimer{
c: c,
init: true,
timer: timer{
when: when,
period: period,
f: f,
arg: arg,
},
}
scheduleLog("new timer")
addTimer(&timerNode{
timer: &tim.timer,
callback: timerCallback,
})
return tim
}
//go:linkname stopTimer time.stopTimer
func stopTimer(tim *timeTimer) bool {
return removeTimer(&tim.timer) != nil
}
//go:linkname resetTimer time.resetTimer
func resetTimer(t *timeTimer, when, period int64) bool {
n := removeTimer(&t.timer)
removed := n != nil
if n == nil {
n = new(timerNode)
}
t.timer.when = when
t.timer.period = period
n.timer = &t.timer
n.callback = timerCallback
addTimer(n)
return removed
}
//go:linkname time_runtimeNano time.runtimeNano
func time_runtimeNano() int64 {
// Note: we're ignoring sync groups here (package testing/synctest).
// See: https://github.com/golang/go/issues/67434
return nanotime()
}
//go:linkname time_runtimeNow time.runtimeNow
func time_runtimeNow() (sec int64, nsec int32, mono int64) {
// Also ignoring the sync group here, like time_runtimeNano above.
return now()
}
// timerNode is an element in a linked list of timers.
type timerNode struct {
next *timerNode
timer *timer
callback func(node *timerNode, delta int64)
// The following fields are only used by schedulers that run timer
// callbacks concurrently with user goroutines (the threads and cores
// schedulers). They make it possible to stop or reset a periodic timer (a
// ticker) while its callback is running, without the callback re-adding the
// timer to the queue afterwards. They are protected by the scheduler's
// timer lock.
//
// firingNext links nodes whose callback is currently running into the
// firingTimers list. stopped is set when the timer was stopped or reset
// while its callback was running, so that timerCallback does not re-add it.
firingNext *timerNode
stopped bool
}
// whenTicks returns the (absolute) time when this timer should trigger next.
func (t *timerNode) whenTicks() timeUnit {
return nanosecondsToTicks(t.timer.when)
}
// timerCallback is called when a timer expires. It makes sure to call the
// callback in the time package and to re-add the timer to the queue if this is
// a ticker (repeating timer).
// This is intentionally used as a callback and not a direct call (even though a
// direct call would be trivial), because otherwise a circular dependency
// between scheduler, addTimer and timerQueue would form. Such a circular
// dependency causes timerQueue not to get optimized away.
// If timerQueue doesn't get optimized away, small programs (that don't call
// time.NewTimer etc) would still pay the cost of these timers.
func timerCallback(tn *timerNode, delta int64) {
// Run timer function (implemented in the time package).
// The seq parameter to the f function is not used in the time
// package so is left zero.
tn.timer.callCallback(delta)
// If this is a periodic timer (a ticker), re-add it to the queue.
if tn.timer.period != 0 {
reAddTimer(tn)
}
}
//go:linkname time_runtimeIsBubbled time.runtimeIsBubbled
func time_runtimeIsBubbled() bool {
// We don't currently support bubbles.
return false
}