time.Ticker的表现

无法找出在下面的 for 循环中我们花费了超过 10 微秒的时间,以至于我们错过了大量的滴答声?

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    RunTicker(time.Millisecond, 10 * time.Second) // Scenario 1
    RunTicker(10 * time.Microsecond, 10 * time.Second) // Scenario 2
}

func RunTicker(tickerInterval, tickerDuration time.Duration) {
    var counter int

    ctx, can := context.WithTimeout(context.Background(), tickerDuration)
    defer can()

    ticker := time.NewTicker(tickerInterval)

exitfor:
    for {
        select {
        case <-ticker.C:
            counter++
        case <- ctx.Done():
            ticker.Stop()
            break exitfor
        }
    }

    fmt.Printf("Tick interval %v and running for %v.Expected counter: %d but got %dn", tickerInterval, tickerDuration, tickerDuration/tickerInterval, counter)
}

输出:

Tick interval 1ms and running for 10s.Expected counter: 10000 but got 9965
Tick interval 10µs and running for 10s.Expected counter: 1000000 but got 976590

回答

一般来说,当 API 说一个事件需要 X 秒时,它实际上保证了经过的时间至少是X 秒,而不是更少。特别是对于小的时间增量,这是一个重要的区别。

另外,请考虑 NewTicker文档中的这一点:

滴答的周期由持续时间参数指定。自动收报机将调整时间间隔或滴答滴答以弥补接收慢的情况。

考虑到这两点,您真正拥有的唯一保证是实际滴答数将 <= 到您计算的预期数字,仅此而已。换句话说,仅在理想情况下实际滴答数 == 预期滴答数,其他所有情况都小于此值。

在这些类型的小时间增量(~< 1ms)中,除了“用户代码”之外,可能还有其他超过滴答时间的事件,包括:

  • Goroutine 调度逻辑(休眠和恢复 goroutine,线程切换)。
  • 垃圾收集(即使在循环期间没有产生垃圾,GC 可能仍然“活着”并且偶尔会检查垃圾)

这些其他因素可能会重合,使得跳过或延迟一个滴答声的可能性更大。

把它想象成你有一个装满水的桶,你需要把它倒进另一个桶里,然后再倒一个桶,再倒一个,以此类推,一千桶。你唯一能做的就是失去水,一旦洒了,你就再也得不到了。在这种情况下,您永远不会期望将 100% 的水保留到最后。

这与您提到的情况类似,因为错误只向一个方向发展。延迟只能至少是指定的时间,并且滴答只能丢失(永远不会获得额外的)。每当这些事件发生时,就好像失去了一滴水。

  • 即使没有滴答滴答,第一点仍然很重要,即时间保证只是“至少这么多时间”,而不是“正好这么多时间”。例如,在我的机器上,将自动收报机时间设置为低于约 15 毫秒没有任何影响(不会超过 15 毫秒)。实际上,总会有一个足够小的时间间隔,以至于您无法准确地延迟这段时间。可以在很小的时间间隔内保持什么样的精度水平可能取决于硬件/操作系统环境。

以上是time.Ticker的表现的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>