4.4 定时器

定时器是进程规划自己在未来某一时刻接获通知的一种机制。本节介绍两种定时器:Timer(到达指定时间触发且只触发一次)和 Ticker(间隔特定时间触发)。

Timer

内部实现源码分析

Timer 类型代表单次时间事件。当 Timer 到期时,当时的时间会被发送给 C (channel),除非 Timer 是被 AfterFunc 函数创建的。

注意:Timer 的实例必须通过 NewTimerAfterFunc 获得。

类型定义如下:

  1. type Timer struct {
  2. C <-chan Time // The channel on which the time is delivered.
  3. r runtimeTimer
  4. }

C 已经解释了,我们看看 runtimeTimer。它定义在 sleep.go 文件中,必须和 runtime 包中 time.go 文件中的 timer 必须保持一致:

  1. type timer struct {
  2. i int // heap index
  3. // Timer wakes up at when, and then at when+period, ... (period > 0 only)
  4. // each time calling f(now, arg) in the timer goroutine, so f must be
  5. // a well-behaved function and not block.
  6. when int64
  7. period int64
  8. f func(interface{}, uintptr)
  9. arg interface{}
  10. seq uintptr
  11. }

我们通过 NewTimer() 来看这些字段都怎么赋值,是什么用途。

  1. // NewTimer creates a new Timer that will send
  2. // the current time on its channel after at least duration d.
  3. func NewTimer(d Duration) *Timer {
  4. c := make(chan Time, 1)
  5. t := &Timer{
  6. C: c,
  7. r: runtimeTimer{
  8. when: when(d),
  9. f: sendTime,
  10. arg: c,
  11. },
  12. }
  13. startTimer(&t.r)
  14. return t
  15. }

when 表示的时间到时,会往 Timer.C 中发送当前时间。when 表示的时间是纳秒时间,正常通过 runtimeNano() + int64(d) 赋值。跟上一节中讲到的 now() 类似,runtimeNano() 也在 runtime 中实现(runtime · nanotime):

  • 调用系统调用 clock_gettime 获取时钟值(这是 POSIX 时钟)。其中 clockid_t 时钟类型是 CLOCK_MONOTONIC,也就是不可设定的恒定态时钟。具体的是什么时间,SUSv3 规定始于未予规范的过去某一点,Linux 上,始于系统启动。
  • 如果 clock_gettime 不存在,则使用精度差些的系统调用 gettimeofday

f 参数的值是 sendTime,定时器时间到时,会调用 f,并将 argseq 传给 f

因为 Timer 是一次性的,所以 period 保留默认值 0。

定时器的具体实现逻辑,都在 runtime 中的 time.go 中,它的实现,没有采用经典 Unix 间隔定时器 setitimer 系统调用,也没有 采用 POSIX 间隔式定时器(相关系统调用:timer_createtimer_settimetimer_delete),而是通过四叉树堆 (heep) 实现的(runtimeTimer 结构中的 i 字段,表示在堆中的索引)。通过构建一个最小堆,保证最快拿到到期了的定时器执行。定时器的执行,在专门的 goroutine 中进行的:go timerproc()。有兴趣的同学,可以阅读 runtime/time.go 的源码。

Timer 相关函数或方法的使用

通过 time.After 模拟超时:

  1. c := make(chan int)
  2. go func() {
  3. // time.Sleep(1 * time.Second)
  4. time.Sleep(3 * time.Second)
  5. <-c
  6. }()
  7. select {
  8. case c <- 1:
  9. fmt.Println("channel...")
  10. case <-time.After(2 * time.Second):
  11. close(c)
  12. fmt.Println("timeout...")
  13. }

time.Stop 停止定时器 或 time.Reset 重置定时器

  1. start := time.Now()
  2. timer := time.AfterFunc(2*time.Second, func() {
  3. fmt.Println("after func callback, elaspe:", time.Now().Sub(start))
  4. })
  5. time.Sleep(1 * time.Second)
  6. // time.Sleep(3*time.Second)
  7. // Reset 在 Timer 还未触发时返回 true;触发了或 Stop 了,返回 false
  8. if timer.Reset(3 * time.Second) {
  9. fmt.Println("timer has not trigger!")
  10. } else {
  11. fmt.Println("timer had expired or stop!")
  12. }
  13. time.Sleep(10 * time.Second)
  14. // output:
  15. // timer has not trigger!
  16. // after func callback, elaspe: 4.00026461s

如果定时器还未触发,Stop 会将其移除,并返回 true;否则返回 false;后续再对该 Timer 调用 Stop,直接返回 false。

Reset 会先调用 stopTimer 再调用 startTimer,类似于废弃之前的定时器,重新启动一个定时器。返回值和 Stop 一样。

Sleep 的内部实现

查看 runtime/time.go 文件中的 timeSleep 可知,Sleep 的是通过 Timer 实现的,把当前 goroutine 作为 arg 参数(getg())。

Ticker 相关函数或方法的使用

TickerTimer 类似,区别是:Ticker 中的 runtimeTimer 字段的 period 字段会赋值为 NewTicker(d Duration) 中的 d,表示每间隔 d 纳秒,定时器就会触发一次。

除非程序终止前定时器一直需要触发,否则,不需要时应该调用 Ticker.Stop 来释放相关资源。

如果程序终止前需要定时器一直触发,可以使用更简单方便的 time.Tick 函数,因为 Ticker 实例隐藏起来了,因此,该函数启动的定时器无法停止。

定时器的实际应用

在实际开发中,定时器用的较多的会是 Timer,如模拟超时,而需要类似 Tiker 的功能时,可以使用实现了 cron spec 的库 cron,感兴趣的可以参考文章:《Go 语言版 crontab》

导航