Timer.Reset() (again...)

617 views
Skip to first unread message

Neil Schellenberger

unread,
Apr 1, 2020, 2:04:38 PM4/1/20
to golang-nuts
Hi Folks,

I am almost certainly overthinking this or in some other way "doing it wrong"--so please (dis)abuse me.

The Timer.Reset() documentation reads (in part) "Reset should be invoked only on stopped or expired timers with drained channels."

Am I correct in understanding that "should" in that sentence means that if neither condition holds then calling Reset() nevertheless still results in legal, well defined behaviour?  I.e. "should" (lest you e.g. unintentionally race) not "must" (lest you e.g. hang).

From inspection of the implementation it appears that the only real consequence of the inherent race is that it might result in either the "old" or "new" expiry being delivered on the next receive.  And even if the "old" expiry is still pending in the (buffered, size one) channel when the runtime attempts to deliver the "new" expiry, the "new" expiry is simply dropped rather than causing any type of deadlock or similar grief to the runtime (i.e. per sendTime(), literally the same callback as for time.Ticker).

I apologise in advance for further belabouring this hoary chestnut ([1][2][3] et multa al. e.g. in StackExchange, Reddit etc.) but I've gotten myself thoroughly confused as to what is guaranteed by the specification.  Taken precisely as written, the admonishments are simply cautions--particularly when read in conjunction with the Timer.Stop() documentation (which uses less proscriptive lanaguage).  But they are so lengthy that they left me with the nagging sense that there might be something more subtle and dire lurking behind that I was failing to grasp.  (I.e. no matter how carefully written the documentation some b*gger--me--will always misunderstand, misinterpret, or otherwise misconstrue it.)

Regards,
Neil

Ian Lance Taylor

unread,
Apr 1, 2020, 4:39:45 PM4/1/20
to Neil Schellenberger, golang-nuts
You are correct: calling Reset on a timer that is neither stopped nor
expired will not cause any sort of deadlock, it will just lead to a
race for your program. The issue is simply that the only point of a
timer is for it to send some value on a channel. If you call Reset on
a timer that is neither stopped nor expired, then you have no idea
whether the value sent on the channel is from the old timer expiration
or the new timer expiration. If it's from the old timer expiration,
you will eventually get another one from the new timer expiration, but
you don't know whether that will happen or not.

If your program doesn't care about that, then it's fine to go ahead
can call Reset on an active timer. But it's hard to understand why a
program wouldn't care. Unless, I suppose, the timers are far in the
future and you can neglect the case of a goroutine pausing for that
long.

Ian

Neil Schellenberger

unread,
Apr 1, 2020, 5:17:14 PM4/1/20
to golang-nuts
Thank you very much for confirming that, Ian!

FWIW the scenario is very roughly along the lines of a "best effort re-configurable" timeout for a work loop:

timeout := <-timeoutConfigC
timer
:= time.NewTimer(timeout)
defer timer
.Stop()
for {
 
select {
   
case timeout = <-timeoutConfigC:
      timer
.Reset(timeout) // if the user's new preference "just misses" this interval, oh well, next time then
   
case <-timer.C:
      handleTimeout
()
      timer
.Reset(timeout)
   
case work := <-workC:
     
if done := do(work); done {
       
return
     
}
 
}
}

The actual use case is a protocol state machine with various user tunable intervals.  It's fine if a new value doesn't get "applied" until the "next time around".  

Obviously there are plenty of other ways to handle this, but it got me wondering if I understood "Timer.Reset()" properly or not.  And/or I may be using an anti-pattern.  Do please let me know if I am :-)

Leszek Kubik

unread,
Apr 4, 2020, 4:31:55 AM4/4/20
to golang-nuts
I have nothing to comment about timer.Reset() but your pseudo code made me wonder if there's an interval timer available in the library, and of course it is.

In the example you aim to do some work periodically, until another type of work succeeds. That's not a plain meaning of a timeout. In addition you may be interested in regular intervals not just "no sooner than x" so NewTicker sounds about right here. If occasionally the timeout preference may change, it's ok to start a new ticker.

timeout := <-timeoutConfigC
ticker := time.NewTicker(timeout)
defer ticker.Stop()
for {
select {
case timeout = <-timeoutConfigC:
ticker.Stop()
ticker = time.NewTicker(timeout) // if the user's new preference "just misses" this interval, oh well, next time then
case <-ticker.C:
handleTimeout() //periodic work 'a'
Reply all
Reply to author
Forward
0 new messages