定时器 Timer 和 Ticker
一、1 次性定时器 Timer
1.Timer 简介
Timer 经过指定的时间后触发一个事件,这个事件通过其本身提供的 channel 进行通知。之所以叫单一事件,是因为 Timer 只执行一次就结束。
通过 timer.NewTimer(d Duration)可以创建一个 Timer,参数既等待的时间,时间到来后立即触发一个事件。
Timer 的数据结构
type Timer struct { C <- chan Time r runtimeTimer }
Timer 对外仅暴露一个 channel,指定的时间到来时就该往 channel 中写入系统时间,既一个事件
2.使用场景
(1)设定超时时间
从一个连接中等待数据
func WaitChannel (conn <- chan string) bool{ timer := time.NewTimer(1 * time.Second) select { case <- conn: timer.Stop() return true case <- timer.C: println("WaitChannel timeout!") return false } }
WaitChannel 的作用就是检测指定的管道中是否有数据到来,通过 select 语句轮询 conn 和 timer.C 两个管道,timer 会在 1s 后向 timer.C 写入数据,如果 1s 内 conn 还没有数据,则判断为超时。
(2)延迟执行某个方法
希望某个方法在今后的某个时间刻执行
func DelayFunction(){ timer := time.NewTimer(5 * time.Second) select { case <- timer.C: log.Panicln("Delayed 5s, start to do something.") } }
DelayFunction()会一直等待 timer 的事件到来后才会执行后面的方法
3.Timer 对外接口
(1)创建定时器
使用 func NewTimer(d Duration) *Timer 方法指定一个时间既可创建一个 Timer,Timer 一经创建便开始计时,不需要额外的启动命令
实际撒谎那个,创建 Timer 意味着把一个计时人物交给系统守护协程,该协程管理着所有的 Timer,当 Timer 的时间到达后向 Timer 的管道中发送当前的时间作为事件。
(2)重置定时器
已过期的定时器或已停止的定时器可以通过重置动作重新激活,重置方法如下:
func (t *Timer) Reset (d Duration) bool
重置动作实质上是先停止定时器,再启动,其返回值既停止计时器(Stop())的返回值。
需要注意的是,重置定时器虽然可以用于修改还未超时的定时器,但正确的使用方式还是针对已过期的定时器或已被停止的定时器,同时其返回值也不可靠,返回值存在的价值仅仅是与前面的版本兼容
实际上,重置定时器意味着通知系统守护协程移除该定时器,重新设置时间后,再把定时器交给守护协程
4.简单接口
除了标准接口,time 包同时还提供了一些简单的方法,在特定的场景下可以简化代码
(1)After()
有时候我们就是想等待指定的时间,没有提前停止定时器的需求,也没有复用该定时器的需求,那么可以使用匿名的定时器
使用 func After(d Duration) <- chan Time 方法创建一个定时器,并返回定时器的管道
func AfterDemo() { log.Println(time.Now()) <-time.After(1 * time.Second) log.Println(time.Now()) }
两条打印的时间间隔为 1s,实际上还是一个定时器,但代码变得更加简洁
(2)AfterFunc()
前面的例子中讲到延迟一个方法的调用,实际上通过 AfterFunc 可以更简洁,而且可以自定义执行的方法,AfterFunc 的原型为:
func AfterFunc (d Duration, f func()) *Timer
该方法在指定时间到来后会执行函数 f.例如:
func AfterFuncDemo(){ log.Println("AfterFuncDemo start: ", time.Now()) time.AfterFunc(1 * time.Second, func() { log.Println("AfterFuncDemo end :",time.Now()) }) time.Sleep(2 * time.Second) //等待协程退出 }
AfterFuncDemo()中先打印了一个时间,然后使用 AfterFunc 启动一个定时器,并指定定时器结束时执行一个方法打印结束时间
与上面例子不同的是,time.AfterFunc()是异步执行的,所以需要函数最后“sleep”等待指定的协程退出,否则可能函数结束时协程还未执行
5.小结
- time.NewTimer(d):创建一个 Timer
- timer.Stop():停止当前 Timer
- time.Reset(d):重置当前 Timer
二、周期性定时器 Ticker
1.Ticker 简介
Ticker 是周期性定时器,既周期性地触发一个事件,通过 Ticker 本身提供的管道将时间传递出去。
Ticker 的数据结构与 Timer 非常类似:
type Ticker struct{ c <-chan Time r runtimeTimer }
Ticker 对外仅暴露一个 channel,指定的时间到来时就往该 channel 中写入系统时间,既一个事件
在创建 Ticker 时会指定一个时间,作为事件触发的周期,这也是 Ticker 与 Timer 最主要的区别,另外,Ticker 的英文原意是钟表的”滴答“声,钟表周期性地产生”滴答“声,既周期性地产生事件
2.使用场景
(1)简单定时任务
有时候希望定时的执行一个任务,这时就可以使用 Ticker 来实现
每隔 1s 记录一次日志:
func TickerDemo() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for range ticker.C { log.Println("Ticker tick.") } }
上述代码中,for range ticker.C 会持续从管道中获取事件,收到事件后打印一行日志,如果管道中没有数据则会阻塞等待事件,由于 Ticker 会周期性地向管道中写入事件,所以上述程序会周期性地打印日志
(2)定时聚合任务
有时我们希望把一些任务打包进行批量处理,比如,公交车发车场景:
- 公交车每隔 5 分钟发一班,不管是否已坐满乘客
- 已坐满乘客情况下,不足 5 分钟也发车
//用于演示聚合任务用法 func TickerLaunch(){ ticker := time.NewTicker(5 * time.Minute) maxPassenger := 30 //最大乘客 passengers := make([]string,0,maxPassenger) for { passenger := GetNewPassenger() //获取一个新乘客 if passenger != ""{ passengers = append(passengers,passenger) }else { time.Sleep(1 * time.Second) } select { case <- ticker.C: //时间到,发车 Launch(passengers) passengers = []string{} default: if len(passengers) >= maxPassenger{ //时间没到,车已经坐满,发车 Launch(passengers) passengers = []string{} } } } }
上面的代码中 for 循环负责接待乘客上车,并决定是否要发车。每当有乘客上车,select 语句会先判断 ticker.C 中是否有数据,有数据则代表发车事件已到,如果没有数据,则判断车是否已坐满,坐满后仍然发车
3.Ticker 对外接口
(1)创建定时器
使用 NewTicker 方法就可以创建一个周期性定时器,函数原型如下:
func NewTicker (d Duration) *Ticker
其中参数 d 为定时器事件触发的周期
(2)停止定时器
使用定时器对外暴露的 Stop 方法就可以停止一个周期性定时器,函数原型如下:
func (t *Ticker) Stop()
需要注意的是,该方法会停止计时,意味着不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完,生命周期结束后会自动释放。
Ticker 在使用完后务必要释放,否者会产生资源泄露,进而会持续消耗 CPU 资源,最后会把 CPU 资源耗尽。
4.简单接口
在有些场景下,我么你启动一个定时器后该定时器永远不会停止,比如定时轮询任务,此时可以使用一个简单的 Tick 函数来获取定时器的管道,函数原型如下:
func Tick(d Duration) <- chan Time
这个函数内部实际上还是创建了一个 Ticker,但并不会返回,所以没有手段来停止该 Ticker,所以,一定要考虑具体的使用场景
5.错误示例
Ticker 用于 for 循环时,很容易出现意想不到的资源泄露问题
资源泄露:
func WrongTicker(){ for { select { case <- time.Tick(1 * time.Second): log.Printf("Resource leak!") } } }
上面的代码中,select 每次检测 case 语句时都会创建一个定时器,for 循环又会不断地执行 select 语句,所以系统里会有越来越多的定时器不断地消耗 CPU 资源,最终 CPU 资源会被耗尽
6.小结
- 使用 time.NewTicker()创建一个定时器
- 使用 Stop()停止一个定时器
- 定时器使用完毕要释放,否则会产生资源泄露