我们经常会遇到这样的场景,周期的检查配置文件是否更新,日志是否轮转,以及定时触发回调函数来清理和回收某些资源等。这就用到了 Ticker 和 Timer。

一,Ticker

周期性的,每隔固定时间间隔,将当前的时间结构发送给 channel。

  1. // Sample 1
  2. t := time.NewTicker(2 * time.Second)
  3. for now := range t.C {
  4. fmt.Println("tick", now)
  5. // event handling
  6. }
  7. // Sample 2
  8. c := time.Tick(2 * time.Second)
  9. for now := range c {
  10. fmt.Println("tick", now)
  11. // event handling
  12. }

上述两个例子均为每隔两秒打印时间信息。输出如下:

  1. tick 2020-03-22 12:00:45.401853048 +0800 CST m=+2.000857230
  2. tick 2020-03-22 12:00:47.401861505 +0800 CST m=+4.000865702
  3. tick 2020-03-22 12:00:49.401863171 +0800 CST m=+6.000867370
  4. tick 2020-03-22 12:00:51.401889229 +0800 CST m=+8.000893410

for…range 依次读取 channel,如果事件处理在时间间隔(两秒)内完成,则会均匀的接收到当时的时间信息。反过来,如果事件处理超过了时间间隔(两秒),Ticker 会自动调整时间间隔以适应慢的接收者。

另一个例子能够停止的 Ticker,

  1. ticker := time.NewTicker(50 * time.Millisecond)
  2. exitC := make(chan bool)
  3. go func() {
  4. for {
  5. select {
  6. case t := <-ticker.C:
  7. fmt.Println("tick", t)
  8. // event handling
  9. case <-exitC:
  10. fmt.Println("exit the ticker")
  11. return
  12. }
  13. }
  14. }()
  15. time.Sleep(5 * time.Second)
  16. ticker.Stop() // stop the Ticker goroutine for garbage collected
  17. exitC <- true // signal to exit

for…select 常被用于 goroutine 能有机会优雅的退出。我们不能强制让一个 goroutine 推出,只能等待它自己结束。对于 exitC,写入数据或者关闭 channel,都可以触发 case <-exitC,进而让 goroutine 退出。当有多个 goroutine 阻塞在同一个 channel 上,关闭这个 channel 比多次写入数据,更加合理,高效。

二,Timer

定时器,在一定时间间隔后触发。与 Ticker 一样,采用最小堆实现。创建 timer后,会被添加到这个最小堆上,在固定的时间间隔后,单独的 goroutine 会被唤醒,读取堆顶的 timer,执行 timer 对应的函数。并从堆顶移除该 timer 。

1. 通过 time.After,实现 timer。

  1. exitC := make(chan bool)
  2. go func() {
  3. // event handling
  4. exitC <- true
  5. }()
  6. select {
  7. case <-exitC:
  8. fmt.Println("event is done")
  9. case <-time.After(15 * time.Second):
  10. fmt.Println("event is timeout")
  11. }

一个单独的 goroutine 用来做事件处理,当事件完成后,写入数据到标识结束的 channel。select 缺省的行为是阻塞,一是监听标识结束的 channel,二是监听 time.After 生成的,标识超时的 channel。也就意味着有两种可能,

  • 事件处理提前结束,select…case 从case 1 退出,那时间 Goroutine 呢?会永远阻塞而 leak 吗?
  • 事件处理超时,select…case 从case 2 退出,时间 Goroutine 完成使命,写入时间信息到 channel 后退出,事件处理 Goroutine 自己慢慢运行结束退出。

我们看看源代码,

  1. // After waits for the duration to elapse and then sends the current time
  2. // on the returned channel.
  3. // It is equivalent to NewTimer(d).C.
  4. // The underlying Timer is not recovered by the garbage collector
  5. // until the timer fires. If efficiency is a concern, use NewTimer
  6. // instead and call Timer.Stop if the timer is no longer needed.
  7. func After(d Duration) <-chan Time {
  8. return NewTimer(d).C
  9. }
  10. // NewTimer creates a new Timer that will send
  11. // the current time on its channel after at least duration d.
  12. func NewTimer(d Duration) *Timer {
  13. c := make(chan Time, 1)
  14. t := &Timer{
  15. C: c,
  16. r: runtimeTimer{
  17. when: when(d),
  18. f: sendTime,
  19. arg: c,
  20. },
  21. }
  22. startTimer(&t.r)
  23. return t
  24. }

time.After 返回一个 channel,注意这是一个带 buffer 的 channel,也就意味着,等到超时后即使没有接收者,时间 Goroutine 仍旧能够写入时间信息,然后优雅的退出。所以这两种可能,都不会造成 Goroutine 的 leak。但是第一种可能,有一点资源浪费,事件处理已经提前结束,就没必要让时间 Goroutine 继续阻塞等待超时,应该让它也提前结束,从堆顶离开。

2. 通过 time.AfterFunc,实现 timer。

  1. func handle() {
  2. timer := time.AfterFunc(8*time.Second, func() {
  3. fmt.Println("Run after 8 seconds")
  4. // callback: cleanup...
  5. })
  6. // when handle is done in 8 seconds, make sure stopping
  7. // the goroutine by time.AfterFunc
  8. defer timer.Stop()
  9. // event handling
  10. }

回调函数将会在新的 Goroutine 中运行,缺省情况下,time.AfterFunc 不会去碰 timer.C,如果我们没有特殊需求的话,timer.C 没有任何用处,可以不考虑(sendTime 会发送时间信息到 channel,这里用 goFunc 在新的 Goroutine 里执行回调函数)。

  1. // AfterFunc waits for the duration to elapse and then calls f
  2. // in its own goroutine. It returns a Timer that can
  3. // be used to cancel the call using its Stop method.
  4. func AfterFunc(d Duration, f func()) *Timer {
  5. t := &Timer{
  6. r: runtimeTimer{
  7. when: when(d),
  8. f: goFunc,
  9. arg: f,
  10. },
  11. }
  12. startTimer(&t.r)
  13. return t
  14. }
  15. func goFunc(arg interface{}, seq uintptr) {
  16. go arg.(func())()
  17. }

如果事件处理在8秒内完成的话,利用 timer.Stop() 来取消 timer,这样可以避免资源的浪费。这种方式相比 time.After 更加灵活。

3. 通过 time.NewTimer,实现 timer。

time.After 是对 time.NewTimer 的二次封装。而time.AfterFunc 直接使用 runtimeTimer。也就是 time.NewTimer 依然会使用 channel 来发送时间信息通过 sendTime 函数。以下是源码,代码不多,注释很长。

  1. // The Timer type represents a single event.
  2. // When the Timer expires, the current time will be sent on C,
  3. // unless the Timer was created by AfterFunc.
  4. // A Timer must be created with NewTimer or AfterFunc.
  5. type Timer struct {
  6. C <-chan Time
  7. r runtimeTimer
  8. }
  9. // NewTimer creates a new Timer that will send
  10. // the current time on its channel after at least duration d.
  11. func NewTimer(d Duration) *Timer {
  12. c := make(chan Time, 1)
  13. t := &Timer{
  14. C: c,
  15. r: runtimeTimer{
  16. when: when(d),
  17. f: sendTime,
  18. arg: c,
  19. },
  20. }
  21. startTimer(&t.r)
  22. return t
  23. }
  24. func sendTime(c interface{}, seq uintptr) {
  25. // Non-blocking send of time on c.
  26. // Used in NewTimer, it cannot block anyway (buffer).
  27. // Used in NewTicker, dropping sends on the floor is
  28. // the desired behavior when the reader gets behind,
  29. // because the sends are periodic.
  30. select {
  31. case c.(chan Time) <- Now():
  32. default:
  33. }
  34. }
  35. // Stop prevents the Timer from firing.
  36. // It returns true if the call stops the timer, false if the timer has already
  37. // expired or been stopped.
  38. // Stop does not close the channel, to prevent a read from the channel succeeding
  39. // incorrectly.
  40. //
  41. // To ensure the channel is empty after a call to Stop, check the
  42. // return value and drain the channel.
  43. // For example, assuming the program has not received from t.C already:
  44. //
  45. // if !t.Stop() {
  46. // <-t.C
  47. // }
  48. //
  49. // This cannot be done concurrent to other receives from the Timer's
  50. // channel or other calls to the Timer's Stop method.
  51. //
  52. // For a timer created with AfterFunc(d, f), if t.Stop returns false, then the timer
  53. // has already expired and the function f has been started in its own goroutine;
  54. // Stop does not wait for f to complete before returning.
  55. // If the caller needs to know whether f is completed, it must coordinate
  56. // with f explicitly.
  57. func (t *Timer) Stop() bool {
  58. if t.r.f == nil {
  59. panic("time: Stop called on uninitialized Timer")
  60. }
  61. return stopTimer(&t.r)
  62. }

我们可以看到 timer.C 是一个带缓冲的 Channel,需要考虑 timer 被 Stop 后 timer.C 的值能够被卸载掉,否则这个 Channel 无法垃圾回收。因此我们需要检查 timer.Stop() 的布尔返回值,这时 timer.C 可能没有卸载掉,也可能卸载掉了。可通过下面代码来完成:

  1. if !timer.Stop() {
  2. select {
  3. case <-timer.C:
  4. default: // 如果 case 阻塞,default 来返回
  5. }
  6. }

这里采用 select 的原因是,有可能 timer.C 已经被读取过了,这样不使用 select 就会阻塞。但是仍有可能存在问题,如果timer.Stop() 运行的时候,时间 Gorountine 还没有来的及将时间信息写入到 timer.C,等到它写入的时候,timer.Stop() 已经运行完毕,错过了。我们在编写程序的时候,需要记录 timer.C 的读取,来作为 timer.Stop() 时的判断。尽量在同一个 Goroutine 中读取 timer.C 和 运行 timer.Stop()。但是 timer.Stop() 并不关闭 timer.C,还是有可能存在问题。我们可以多加一个新的 Channel,用做信号通知用,具体可根据业务逻辑来细化。以下是一个简单例子:

  1. timer := time.NewTimer(8 * time.Second)
  2. var exitCh = make(chan struct{})
  3. go func() {
  4. ...
  5. for {
  6. select {
  7. case <- exitCh:
  8. if !timer.Stop() {
  9. select {
  10. case <-timer.C:
  11. default: // 如果 case 阻塞,default 来返回
  12. }
  13. }
  14. return
  15. case <-timer.C:
  16. // handling timeout
  17. ...
  18. }
  19. }
  20. } ()
  21. // handling event
  22. timer.Stop()
  23. close(exitCh)

参考链接: