multithreadinggotimerticker

Resetting a ticker


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).


Solution

  • Update

    Go 1.15 includes the Ticker.Reset method which eliminates the need for this entire solution.

    Original

    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)
    }