我们经常会遇到这样的场景,周期的检查配置文件是否更新,日志是否轮转,以及定时触发回调函数来清理和回收某些资源等。这就用到了 Ticker 和 Timer。
一,Ticker
周期性的,每隔固定时间间隔,将当前的时间结构发送给 channel。
// Sample 1t := time.NewTicker(2 * time.Second)for now := range t.C {fmt.Println("tick", now)// event handling}// Sample 2c := time.Tick(2 * time.Second)for now := range c {fmt.Println("tick", now)// event handling}
上述两个例子均为每隔两秒打印时间信息。输出如下:
tick 2020-03-22 12:00:45.401853048 +0800 CST m=+2.000857230tick 2020-03-22 12:00:47.401861505 +0800 CST m=+4.000865702tick 2020-03-22 12:00:49.401863171 +0800 CST m=+6.000867370tick 2020-03-22 12:00:51.401889229 +0800 CST m=+8.000893410
for…range 依次读取 channel,如果事件处理在时间间隔(两秒)内完成,则会均匀的接收到当时的时间信息。反过来,如果事件处理超过了时间间隔(两秒),Ticker 会自动调整时间间隔以适应慢的接收者。
另一个例子能够停止的 Ticker,
ticker := time.NewTicker(50 * time.Millisecond)exitC := make(chan bool)go func() {for {select {case t := <-ticker.C:fmt.Println("tick", t)// event handlingcase <-exitC:fmt.Println("exit the ticker")return}}}()time.Sleep(5 * time.Second)ticker.Stop() // stop the Ticker goroutine for garbage collectedexitC <- 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。
exitC := make(chan bool)go func() {// event handlingexitC <- true}()select {case <-exitC:fmt.Println("event is done")case <-time.After(15 * time.Second):fmt.Println("event is timeout")}
一个单独的 goroutine 用来做事件处理,当事件完成后,写入数据到标识结束的 channel。select 缺省的行为是阻塞,一是监听标识结束的 channel,二是监听 time.After 生成的,标识超时的 channel。也就意味着有两种可能,
- 事件处理提前结束,select…case 从case 1 退出,那时间 Goroutine 呢?会永远阻塞而 leak 吗?
- 事件处理超时,select…case 从case 2 退出,时间 Goroutine 完成使命,写入时间信息到 channel 后退出,事件处理 Goroutine 自己慢慢运行结束退出。
我们看看源代码,
// After waits for the duration to elapse and then sends the current time// on the returned channel.// It is equivalent to NewTimer(d).C.// The underlying Timer is not recovered by the garbage collector// until the timer fires. If efficiency is a concern, use NewTimer// instead and call Timer.Stop if the timer is no longer needed.func After(d Duration) <-chan Time {return NewTimer(d).C}// NewTimer creates a new Timer that will send// the current time on its channel after at least duration d.func NewTimer(d Duration) *Timer {c := make(chan Time, 1)t := &Timer{C: c,r: runtimeTimer{when: when(d),f: sendTime,arg: c,},}startTimer(&t.r)return t}
time.After 返回一个 channel,注意这是一个带 buffer 的 channel,也就意味着,等到超时后即使没有接收者,时间 Goroutine 仍旧能够写入时间信息,然后优雅的退出。所以这两种可能,都不会造成 Goroutine 的 leak。但是第一种可能,有一点资源浪费,事件处理已经提前结束,就没必要让时间 Goroutine 继续阻塞等待超时,应该让它也提前结束,从堆顶离开。
2. 通过 time.AfterFunc,实现 timer。
func handle() {timer := time.AfterFunc(8*time.Second, func() {fmt.Println("Run after 8 seconds")// callback: cleanup...})// when handle is done in 8 seconds, make sure stopping// the goroutine by time.AfterFuncdefer timer.Stop()// event handling}
回调函数将会在新的 Goroutine 中运行,缺省情况下,time.AfterFunc 不会去碰 timer.C,如果我们没有特殊需求的话,timer.C 没有任何用处,可以不考虑(sendTime 会发送时间信息到 channel,这里用 goFunc 在新的 Goroutine 里执行回调函数)。
// AfterFunc waits for the duration to elapse and then calls f// in its own goroutine. It returns a Timer that can// be used to cancel the call using its Stop method.func AfterFunc(d Duration, f func()) *Timer {t := &Timer{r: runtimeTimer{when: when(d),f: goFunc,arg: f,},}startTimer(&t.r)return t}func goFunc(arg interface{}, seq uintptr) {go arg.(func())()}
如果事件处理在8秒内完成的话,利用 timer.Stop() 来取消 timer,这样可以避免资源的浪费。这种方式相比 time.After 更加灵活。
3. 通过 time.NewTimer,实现 timer。
time.After 是对 time.NewTimer 的二次封装。而time.AfterFunc 直接使用 runtimeTimer。也就是 time.NewTimer 依然会使用 channel 来发送时间信息通过 sendTime 函数。以下是源码,代码不多,注释很长。
// The Timer type represents a single event.// When the Timer expires, the current time will be sent on C,// unless the Timer was created by AfterFunc.// A Timer must be created with NewTimer or AfterFunc.type Timer struct {C <-chan Timer runtimeTimer}// NewTimer creates a new Timer that will send// the current time on its channel after at least duration d.func NewTimer(d Duration) *Timer {c := make(chan Time, 1)t := &Timer{C: c,r: runtimeTimer{when: when(d),f: sendTime,arg: c,},}startTimer(&t.r)return t}func sendTime(c interface{}, seq uintptr) {// Non-blocking send of time on c.// Used in NewTimer, it cannot block anyway (buffer).// Used in NewTicker, dropping sends on the floor is// the desired behavior when the reader gets behind,// because the sends are periodic.select {case c.(chan Time) <- Now():default:}}// Stop prevents the Timer from firing.// It returns true if the call stops the timer, false if the timer has already// expired or been stopped.// Stop does not close the channel, to prevent a read from the channel succeeding// incorrectly.//// To ensure the channel is empty after a call to Stop, check the// return value and drain the channel.// For example, assuming the program has not received from t.C already://// if !t.Stop() {// <-t.C// }//// This cannot be done concurrent to other receives from the Timer's// channel or other calls to the Timer's Stop method.//// For a timer created with AfterFunc(d, f), if t.Stop returns false, then the timer// has already expired and the function f has been started in its own goroutine;// Stop does not wait for f to complete before returning.// If the caller needs to know whether f is completed, it must coordinate// with f explicitly.func (t *Timer) Stop() bool {if t.r.f == nil {panic("time: Stop called on uninitialized Timer")}return stopTimer(&t.r)}
我们可以看到 timer.C 是一个带缓冲的 Channel,需要考虑 timer 被 Stop 后 timer.C 的值能够被卸载掉,否则这个 Channel 无法垃圾回收。因此我们需要检查 timer.Stop() 的布尔返回值,这时 timer.C 可能没有卸载掉,也可能卸载掉了。可通过下面代码来完成:
if !timer.Stop() {select {case <-timer.C:default: // 如果 case 阻塞,default 来返回}}
这里采用 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,用做信号通知用,具体可根据业务逻辑来细化。以下是一个简单例子:
timer := time.NewTimer(8 * time.Second)var exitCh = make(chan struct{})go func() {...for {select {case <- exitCh:if !timer.Stop() {select {case <-timer.C:default: // 如果 case 阻塞,default 来返回}}returncase <-timer.C:// handling timeout...}}} ()// handling eventtimer.Stop()close(exitCh)
参考链接:
- https://golang.org/pkg/time/#Ticker
- https://stackoverflow.com/questions/36886548/do-repetitive-tasks-at-intervals-in-golang-using-time-afterfunc-just-a-sample
- https://blog.gopheracademy.com/advent-2016/go-timers/
- http://russellluo.com/2018/09/the-correct-way-to-use-timer-in-golang.html
- https://github.com/golang/go/blob/master/src/time/sleep.go
- https://github.com/golang/go/blob/master/src/time/time.go
