Consider a server that is initialised with a timer/ticker resource that will fire every t
(t
is 20ms in my example) ticks. Every time the server listens to something on the network (like a periodic signal from peers), it has to reset the timer. On the other hand if the timer expires without a reset (for example, all of its peers are dead), it triggers some event (I'm just printing the time from start of program in my example).
I'm having trouble implementing this behaviour using time.Ticker
. Resetting the timer seems to work (it does not fire for the first 50ms), but the ticker is not active (doesn't tick every 20ms) after that.
package main
import (
"fmt"
"time"
)
var wallclock time.Time
type server struct {
timeout *time.Ticker
stop chan bool
}
func (srv *server) start() {
for {
select {
case <-srv.timeout.C:
{
elapsed := time.Since(wallclock)
fmt.Println("timed out after ", elapsed, " elapsed from start ")
}
case <-srv.stop:
{
return
}
}
}
}
func main() {
wallclock = time.Now()
//make the server with a timer that will fire every 20ms
srv := server{
timeout: time.NewTicker(20 * time.Millisecond),
//channel to indicate the server to stop listening
stop: make(chan bool),
}
//start listening on a different thread
go srv.start()
for i := 0; i < 5; i++ {
//reset it every 10ms
time.Sleep(10 * time.Millisecond)
srv.timeout.Stop()
//as the reset frequency is higher,
//I'm not expecting this to fire within
//the first 50ms (5*10ms)
srv.timeout = time.NewTicker(20 * time.Millisecond)
}
//sleep for 110ms
//I'm expecting the timer to fire at least 5 times here
time.Sleep(110 * time.Millisecond)
//stop listening
srv.stop <- true
fmt.Println("Hi from tckr!")
}
I'm expecting to see something like
timed out after ~70ms elapsed from start
timed out after ~90ms elapsed from start
timed out after ~110ms elapsed from start
timed out after ~130ms elapsed from start
timed out after ~150ms elapsed from start
Hi from tckr!
Five times because I let the main thread sleep for 110ms and the 20ms timer could fire five times within this interval.
But I just see Hi from tckr!
. Is srv.timeout = time.NewTicker(20 * time.Millisecond)
the proper way to reset Ticker
?
If I don't stop the ticker within the for
loop (srv.timeout.Stop()
), the ticker seems to keep ticking. Here's a sample output after commenting srv.timeout.Stop()
.
timed out after 20.6872ms elapsed from start
timed out after 41.4278ms elapsed from start
timed out after 61.8747ms elapsed from start
timed out after 72.7793ms elapsed from start
timed out after 94.1448ms elapsed from start
timed out after 112.5283ms elapsed from start
timed out after 134.0131ms elapsed from start
timed out after 152.5846ms elapsed from start
Hi from tckr!
I don't want the ticker to fire within the first 50ms (i.e, I don't want to see the first two lines with 20.6872ms and 41.4278ms).
Go 1.15 includes the Ticker.Reset
method which eliminates the need for this entire solution.
Remember, for select
statements:
For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the "select" statement. The result is a set of channels to receive from or send to, and the corresponding values to send.
That means that once start
enters its select
statement, it evaluates srv.timeout.C
and holds onto the channel; any change to srv.timeout
after it enters the select
will have no impact on the select
, it will still be waiting to receive from the channel it had before.
You could get around this by adding another channel (in the example here named ping
) so that you can signal to start
that the channel is changing (or possibly move the entire reset logic into start
):
type server struct {
timeout *time.Ticker
stop chan bool
ping chan struct{}
}
func (srv *server) start() {
for {
select {
case <-srv.timeout.C:
elapsed := time.Since(wallclock)
fmt.Println("timed out after ", elapsed, " elapsed from start ")
case <-srv.ping:
// do nothing & let the loop iterate
// OR
srv.timeout.Stop()
srv.timeout = time.NewTicker(20 * time.Millisecond)
case <-srv.stop:
return
}
}
}
// in main()
go srv.start()
for i := 0; i < 5; i++ {
//reset it every 10ms
time.Sleep(10 * time.Millisecond)
srv.ping <- struct{}{}
// possibly shift the below logic to start()'s ping handler case
srv.timeout.Stop()
srv.timeout = time.NewTicker(20 * time.Millisecond)
}