Golang中time.Timer的精度问题探讨

Golang中time.Timer的精度问题探讨

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

有时会出现一分钟内触发两次的情况,因为持续时间可能会缩短。这真的很奇怪。有人能帮我吗?谢谢

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

更多关于Golang中time.Timer的精度问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

请注意,time.Truncate 方法会向下取整,因此您可能不是将完整的一分钟添加到当前时间,而是添加稍少的时间。这样理解对吗?

更多关于Golang中time.Timer的精度问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢回复!那么您说的"向下取整"是什么意思?我不太理解为什么它会少加一点。您能帮我解释一下吗 :)

最初我以为你问的是为什么没有精确休眠一分钟。是我的错。

假设现在是12:15:30.91。

调用Truncate后得到12:15:30.00。

现在增加1分钟:12:16:30.00。

再减去当前时间:00:00:59.09。

既然你醒来的时间只提前了不到1毫秒,我怀疑这是由于计算开销和计时器管理机制造成的。如果有影响的话,你应该是会晚醒才对。我也怀疑这与单调时钟有关,但有可能。这是在什么类型的硬件上运行的?

纯粹出于好奇,如果你使用通道传递的时间而不是time.Now(),会得到什么结果?

抱歉,我一直在错过你的问题。我最初的回答适用于这两行代码:

2018-07-19 14:39:59.999490194 +0800 CST m=+264.919412884
2018-07-19 14:40:00.000167519 +0800 CST m=+264.920090231

注意当你进行计算时,当前时间是14:39:59.99,截断操作返回14:39:59.00,你加上一分钟得到14:40:00,然后减去当前时间得到00:00:00.001,这就是你需要等待的时长,所以你在一毫秒后再次唤醒。你能在下一毫秒流逝前成功挤入Reset调用的这个事实真是令人惊讶!

问题出在calDuration函数计算下一个整分钟时可能返回负值,导致计时器立即触发。当系统时间接近整分钟边界时,TruncateSub的组合可能产生负持续时间,因为time.Now()在计算过程中略有延迟。

以下是修正后的代码:

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

关键修改:

  1. 确保calDuration总是返回正值
  2. 使用指针类型的sync.WaitGroup
  3. 添加defer t.Stop()确保资源释放
  4. 使用select语句提供更好的控制

如果仍然遇到精度问题,可以考虑使用time.Ticker作为替代方案:

func TestTickerFixed(wg *sync.WaitGroup) {
    go func(){
        defer wg.Done()
        // 先等待到下一个整分钟
        now := time.Now()
        firstDelay := now.Truncate(time.Minute).Add(time.Minute).Sub(now)
        
        time.Sleep(firstDelay)
        
        ticker := time.NewTicker(time.Minute)
        defer ticker.Stop()
        
        for {
            select {
            case <-ticker.C:
                fmt.Println(time.Now())
            }
        }
    }()
}

time.Ticker内部已经处理了时间漂移问题,通常能提供更稳定的定时触发。

回到顶部