golang的定时器虽然使用起来很简单,但是依然又一些细节需要注意的。
首先来看一个例子
func s1() {
var count int
for {
select {
case <-time.Tick(time.Second * 1):
fmt.Println("case1")
count++
fmt.Println("count--->" , count)
case <-time.Tick(time.Second * 2) :
fmt.Println("case2")
count++
fmt.Println("count--->" , count)
}
}
}
执行结果
case1
count---> 1
case1
count---> 2
case1
count---> 3
case1
count---> 4
可见 case2 永远没有被执行到,问题就出在代码逻辑上,首先看time.Tick
方法
func Tick(d Duration) <-chan Time {
if d <= 0 {
return nil
}
return NewTicker(d).C
}
它每次都会创建一个新的定时器,随着 for 循环进行, select 始终监听两个新创建的定时器,老的定时器被抛弃掉了,也就不会去读取老定时器中的通道。
select 可以同时监听多个通道,谁先到达就先读取谁,如果同时有多个通道有消息到达,那么会随机读取一个通道,其他的通道由于没有被读取,所以数据不会丢失,需要循环调用 select 来读取剩下的通道。
触发定时器
定时器拥有一个长度为 1 的缓冲通道,这保证了系统可以去触发这个定时器,而不用关心是否有 goroutine 在读取这个通道,触发定时器:
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:
}
}
出于安全考虑,go的定时器触发被设置为非阻塞的,这也好理解,如果定时器被触发后,而应用程序始终不来读取通道信息,那么再次触发定时器岂不是要阻塞在那里,这将极大消耗系统资源,因此选择直接跳过。
回收定时器
那么定时器何时被 gc 呢?
Timer
由于 Timer 是一次性的,一旦被触发了,就会被交给 gc ,但是它的通道要等到被读取后才会被关闭。
其次可以手动调用 Stop
方法,来提前结束定时器,使其被 gc 。如果Stop调用成功停止了timer,则返回true,如果timer已经被触发过或者已经被停止了,则返回false。同样的,通道并不会被关闭。
Ticker
由于 Ticker 是循环触发的,系统并不会自动停止它,所以,必须要手动调用Stop
方法,否则,不光内存得不到释放,cpu也会周期性的被大量占用,即使我们已经不再使用那个Ticker对象了,但是golang底层的定时器还在工作。所以,上面的例子其实是很危险的。必须手动Stop Ticker
。
可以修改为:
func s2() {
t1 := time.NewTicker(time.Second * 1)
t2 := time.NewTicker(time.Second * 2)
defer t1.Stop()
defer t2.Stop()
var count int
for {
select {
case <-t1.C:
fmt.Println("case1")
count++
fmt.Println("count--->" , count)
case <-t2.C :
fmt.Println("case2")
count++
fmt.Println("count--->" , count)
}
}
}
我们经常能看到这样的代码,由于使用的是 Timer ,其实问题不大。
func s3() {
ch := make(chan struct{}, 1)
for {
select {
case <-ch :
//
case <-time.After(time.Second * 1) :
//
}
}
}