rogeryu
rogeryu

Reputation: 29

About the accuracy of the time.Timer

package main


import (
    "time"
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    TestTicker(wg)
    wg.Wait()
}

func TestTicker(wg sync.WaitGroup) {
    calDuration := func(duration time.Duration) time.Duration {
        now := time.Now()
        return now.Truncate(duration).Add(duration).Sub(now)
    }
    go func(){
        t := time.NewTimer(calDuration(time.Minute))
        for {
            <-t.C
            fmt.Println(time.Now())
            t.Reset(calDuration(time.Minute))
        }
        wg.Done()
    }()
}

It sometimes happens to tick twice a minute as the duration may shrink. It's really strange. Could somebody help me. Thanks

I simply use waitgroup to hold the main function while calling TestTicker.

I'm running the test code on my MacOS

2018-07-19 14:36:00.003887996 +0800 CST m=+24.916092657
2018-07-19 14:37:00.002985076 +0800 CST m=+84.917119245
2018-07-19 14:38:00.001214551 +0800 CST m=+144.917278207
2018-07-19 14:39:00.000418561 +0800 CST m=+204.918411736
2018-07-19 14:39:59.999490194 +0800 CST m=+264.919412884
2018-07-19 14:40:00.000167519 +0800 CST m=+264.920090231
2018-07-19 14:40:59.99914446 +0800 CST m=+324.920996684
2018-07-19 14:41:00.000247228 +0800 CST m=+324.922099488

Upvotes: 2

Views: 1667

Answers (1)

Denis Bernard
Denis Bernard

Reputation: 46

The timer accuracy can vary depending on your OS, hardware and CPU load. Virtual Machines seem particularly bad at providing accurate timers (see https://github.com/golang/go/issues/14410). Unfortunately, you do not mention in what environment you're running this code.

If you can live with the inaccuracies, and still need your code do do something at about a full minute, your code breaks because when the interval is too short (14:39:59.999490194 is only 500µs short of 14:40), calDuration will make it wait that few microseconds until the next full minute. In order to fix this you need to use Duration.Round instead of Duration.Truncate.

Also do not forget that t.C returns the time at which the timer fired, so you need to use this value in your call to calDuration (this also saves you a costly syscalls).

func TestTicker(wg *sync.WaitGroup) {
    calDuration := func(now time.Time, duration time.Duration) time.Duration {
        return now.Round(duration).Add(duration).Sub(now)
    }
    go func(){
        t := time.NewTimer(calDuration(time.Now(), time.Minute))
        for {
            now := <-t.C
            fmt.Println(now)
            t.Reset(calDuration(now, time.Minute))
        }
        wg.Done()
    }()
}

Another approach is to use time.Ticker from the standard library and issue an appropriate sleep before starting the ticker so that it ticks on a full minute:

func TestTicker(wg *sync.WaitGroup) {
    go func(interval time.Duration) {
        // wait until next time interval
        now := time.Now()
        time.Sleep(now.Truncate(interval).Add(interval).Sub(now))
        // get time of first beat
        now = time.Now()
        // start the ticker
        t := time.NewTicker(interval)
        for {
            fmt.Println(now)
            now = <-t.C
        }
        wg.Done()
    }(time.Minute)
}

Upvotes: 1

Related Questions