我们经常会遇到这样的场景,周期的检查配置文件是否更新,日志是否轮转,以及定时触发回调函数来清理和回收某些资源等。这就用到了 Ticker 和 Timer。
一,Ticker
周期性的,每隔固定时间间隔,将当前的时间结构发送给 channel。
// Sample 1
t := time.NewTicker(2 * time.Second)
for now := range t.C {
fmt.Println("tick", now)
// event handling
}
// Sample 2
c := 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.000857230
tick 2020-03-22 12:00:47.401861505 +0800 CST m=+4.000865702
tick 2020-03-22 12:00:49.401863171 +0800 CST m=+6.000867370
tick 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 handling
case <-exitC:
fmt.Println("exit the ticker")
return
}
}
}()
time.Sleep(5 * time.Second)
ticker.Stop() // stop the Ticker goroutine for garbage collected
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。
exitC := make(chan bool)
go func() {
// event handling
exitC <- 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.AfterFunc
defer 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 Time
r 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 来返回
}
}
return
case <-timer.C:
// handling timeout
...
}
}
} ()
// handling event
timer.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