time.Ticker 的表现
Performance of 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 %d\n", 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 考虑这一点 documentation:
The period of the ticks is specified by the duration argument. The ticker will adjust the time interval or drop ticks to make up for slow receivers.
考虑到这两点,您真正拥有的唯一保证是实际分时报价的数量 <= 您计算的预期数量,仅此而已。换句话说,实际报价 == 预期报价仅在 理想情况下 ,其他所有情况都将小于此。
在这些小时间增量(~< 1 毫秒)中,除了“用户代码”之外,可能还有其他事件超过滴答时间,包括:
- Goroutine调度逻辑(睡眠和恢复goroutines,线程切换)。
- 垃圾收集(即使在循环期间没有产生垃圾,GC 可能仍然“活跃”并且偶尔会检查垃圾)
这些其他因素可能同时发生,使得跳动被跳过或延迟的可能性更大。
把它想象成你有一个装满水的桶,你需要把它倒进另一个桶,然后是另一个桶,再一个,依此类推,持续 1000 个桶。 只能失水,失而复得。在这种情况下,您永远不会期望将 100% 的水保留到最后。
这与您提到的情况类似,因为错误只朝一个方向发展。延迟只能至少达到指定的时间,并且只能丢失刻度(永远不会获得额外的)。任何时候发生这些事件,就像失去了一滴水。
无法找出在下面的 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 %d\n", 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 考虑这一点 documentation:
The period of the ticks is specified by the duration argument. The ticker will adjust the time interval or drop ticks to make up for slow receivers.
考虑到这两点,您真正拥有的唯一保证是实际分时报价的数量 <= 您计算的预期数量,仅此而已。换句话说,实际报价 == 预期报价仅在 理想情况下 ,其他所有情况都将小于此。
在这些小时间增量(~< 1 毫秒)中,除了“用户代码”之外,可能还有其他事件超过滴答时间,包括:
- Goroutine调度逻辑(睡眠和恢复goroutines,线程切换)。
- 垃圾收集(即使在循环期间没有产生垃圾,GC 可能仍然“活跃”并且偶尔会检查垃圾)
这些其他因素可能同时发生,使得跳动被跳过或延迟的可能性更大。
把它想象成你有一个装满水的桶,你需要把它倒进另一个桶,然后是另一个桶,再一个,依此类推,持续 1000 个桶。 只能失水,失而复得。在这种情况下,您永远不会期望将 100% 的水保留到最后。
这与您提到的情况类似,因为错误只朝一个方向发展。延迟只能至少达到指定的时间,并且只能丢失刻度(永远不会获得额外的)。任何时候发生这些事件,就像失去了一滴水。