定时器
定时器在Go语言开发中被广泛使用,准确掌握其用法和实现原理至关重要
Go语言提供了两种定时器
- 一次性定时器Timer:定时器只计时一次,计时结束便停止运行
- 周期性定时器Ticker:定时器周期性地进行计时,除非主动停止,否则将永久运行。
1 一次性定时器Timer
1.1 简介
Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。
源码如下:
type Timer struct{ //Timer代表一次定时,时间到来后仅发生一个事件
c <-chan Time
r runtimeTimer
}
//通过timer.NewTimer(d Duration)创建一个Timer,参数是等待的时间,时间结束后触发时间
1.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
}
}
2)延迟执行某个方法
func DelayFunction() bool {
timer := time.NewTimer(5 * time.Second)
select {
case <- timer.C:
println("Delayed 5s,start to do something")
}
}
1.3 Timer对外接口
1)创建定时器
func NewTimer(d Duration) *Timer
方法指定一个时间即可创建一个Timer,Timer一经创建便开始计时,不需要额外的启动命令。
创建Timer意味把一个及时任务交给系统守护协程。该协程管理着所有的Timer,当Timer的时间到达后,Timer向管道中发送当前的时间作为事件。
2)停止定时器
Timer创建后可以随时停止:
func (t *Timer) Stop() bool
其返回值代表定时器有没有超时
- true :定时器超时前停止,后续不会再发送事件
- false:定时器超时后停止
实际上,停止计时器意味着通知系统守护协程移除该定时器
3)重置定时器
已过期的定时器或已停止的计时器可以通过重置动作重新激活,重置方法如下:
func(t *Timer) Reset(d Duration) bool
重置的动作实质上是通知系统守护协程移除该定时器,重新设定时间后,再把定时器交给守护协程。
1.4 简单接口
timer包同时还提供了一些简单的方法,在特定的场景下可以优化代码
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更为简介
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) // 等待协程退出
}
上面两个例子都是打印出两个时间,但是time.AfterFunc()是异步执行的,而上一个例子是同步执行的。
2.实现原理
2.1 数据结构
1)Timer
type Timer struct{
C <-chan Time //管道,上层应用根据此管道接收事件
r runtimeTimer //running定时器,该定时器即系统管理的警示器,对上层应用不可见
}
2)runtimeTimer
创建一个Timer实质上是把一个定时任务交给专门的协程进行监控,这个任务的载体便是runtimeTimer。
简单地讲,没创建一个Timer意味着创建了一个runtimeTimer变量,然后把它交给系统进行监控。我们通过设置runtimeTimer过期后的行为来达到定时的目的。
type runtimeTimer struct{
tb uintptr //存储当前定时器的数组地址
i int //存储当前定时器的数组下标
when int64 //当前定时器触发时间
period int64 //当前定时器周期性触发间隔(对于Timer来说,恒为0)
f func(interface{}, uintptr) //定时器触发时执行的回调函数
arg interface{} //定时器触发时执行回调函数传递的参数1
seq uintptr //定时器触发时执行回调函数传递的参数2(该参数只在网络收发场景下使用)
}
2.2 实现原理
一个进程中的多个Timer都由底层的协程来管理,为了描述方面,我们把这个协程称为系统协程。
runtimeTimer存放在数组中,并按照when字段对所有的runtimeTimer进行堆排序,定时器触发时执行runtimeTimer中的预定义函数f,即完成了一次定时任务。
1)创建Timer
func NewTimer(d Duration) *Timer{
c := make(chan Time,1) //创建一个通道
t := &Timer{
C:c,
r:runtimeTimer{
when: when(d), //触发时间
f: sendTime, //触发后执行snedTime函数
arg: c, //触发后执行sendTime函数时附带的参数
},
}
startTimer(&t,r) //启动定时器,只是把runtimeTimer放到系统协程的堆中,由系统协程维护
return t
}
- NewTimer构造了一个Timer
- 把Timer.r通过startTimer交给系统协程维护
- when计算下一次定时器触发的绝对时间,即当前时间+d
- sendTime方法是定时器触发时的动作
func sendTime(c interface{}, seq uintptr){
select{
case c.(chan Time) <- Now():
default:
}
}
创建Timer时生成的管道含有一个缓冲区(make(chan Time, 1)),所以Timer触发时间管道写入事件永远不会阻塞,sendTime写完即退出。
之所以sendTime()使用select并搭配一个空的default分支,是因为后面的Ticker也复用sendTime(),Ticker触发时也会向管道中写入时间,但无法保证之前的数据已被取走,如果管道中还有值,则本次不再向管道中写入时间,将本次触发的事件直接丢弃。
startTimer(&t.r)的具体实现在runtime包中,其主要作用是把runtimeTimer写入系统协程的数组中,并启动系统协程(之后会介绍)
2)停止Timer
- stopTimer即通知系统协程吧该Timer移除,即不再监控。
- 系统协程只是移除Timer,并不会关闭通道,以避免用户协程读取错误
func (t *Timer) Stop() bool{
return stopTimer(&t.r)
}
3)重置Timer
重置Timer时会先把Timer从系统协程中删除,修改新的时间后重新添加到系统协程中。
func (t *Timer) Reset(d Duration) bool{
w := when(d)
active := stopTimer(&t.r)
t.r.when = w
startTimer(&t.r)
return active
}
其返回值与Stop一致,如果Timer成功停止,返回true。如果已经触发,返回false
注意:按照官方的说法,Reset()应该作用于已经停止的Timer或已经触发Timer。其返回值应该总是false,如果不按照次约定使用Reset(),则有可能遇到Reset()和Timer触发后同时执行的情况,此时有可能会收到两个事件,对应用程序造成负面影响。
2.3 小结
- NewTimer()创建一个新的Timer交给系统协程监控
- Stop()通知系统协程删除指定的Timer
- Reset()通知系统协程删除指定的Timer并再添加一个新的Timer
3. 周期性定时器(Ticker)
3.1 简介
Ticker是一种周期性定时器,即周期性触发一个事件,这个事件通过其本身提供的channel进行通知,对外仅暴露一个channel。
源码如下:
type Ticker struct{ //Timer代表一次定时,时间到来后仅发生一个事件
c <-chan Time
r runtimeTimer
}
3.2 使用场景
1)简单的定时任务
下面的代码演示每隔1s记录一次日志
func TickerDemo() {
timer := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C{
log.Println("Ticker tick.")
}
}
2)定时聚合任务
有时我们希望把一些任务打包进行批量处理。比如,公交车发车场景:
- 公交车每隔5分钟发一班,不管是否已坐满乘客
- 已坐满乘客情况下,不足5分钟也发车
func TickerLanch() bool {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
maxPassenger := 30 //没车最大装载人数
passengers := make([]string,0,maxPassenger)
for {
passenger := GetNewPassenger()//获取一个新乘客
if passenger != ""{
passenger = 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{}
}
}
}
}
3.3 Timer对外接口
1)创建定时器
func NewTicker(d Duration) *Ticker
方法指定一个时间即可创建一个Ticker
参数d为定时器事件触发的周期
2)停止定时器
Ticker创建后可以随时停止:
func (t *Ticker) Stop()
该方法会停止计时,意味着不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完,生命周期结束后自动释放。
Ticker在使用完后务必要释放,否则会产生资源泄漏,会持续消耗CPU资源
3.4 简单接口
timer包同时还提供了一些简单的方法,在特定的场景下可以优化代码
1)Tick()
func Tick(d Duration) <-chan Time
返回定时器的管道
函数的内部创建了一个Ticker,但并不会返回,所以没有手段来停止该Ticker
3.5 小结
Ticker的相关内容总结如下:
- 使用time.NewTicker()创建一个定时器
- 使用Stop停止一个定时器
- 定时器使用完毕要释放,否则会产生资源泄漏
4.实现原理
Ticker与之前讲的Timer几乎完全相同,数据结构和实现内部机制都相同,唯一不同的是创建方式。
4.1 数据结构
1)Ticker
数据结构和Timer完全一致
type Ticker struct{
c <-chan Time
r runtimeTimer
}
2)runtimeTimer
也与Timer完全一致
4.2 实现原理
1)创建Ticker
func NewTicker(d Duration) *Ticker{
if d <= 0{
panic(errors.New("non-positive interval for NewTicker"))
}
c := make(chan Time, 1)
t := &Ticker{
c: c,
r: runtimetimer{
when: when(d),
//根据这个产生决定Timer是一次性的,还是周期性的
period: int64(d),//Ticker跟Timer的重要区别就上提供了period参数
f: sendTime,
arg: c,
}
}
startTime(&t.r)
return t
}
NewTicker()只是构造了一个Ticker,然后吧Ticker.r通过startTime交给了系统协程维护,其中period为事件触发的周期,sendTime方法便是定时器触发时的动作
func sendTime(c interface{}, seq uintptr){
select{
case c.(chan Time) <- Now():
default:
}
}
sendTime函数中每隔一段时间就向管道中写入当前时间,如果缓冲区内的数据未被即时取走,那么也不会阻塞,而是直接退出,会造成本次事件丢失的后果。
2)停止Ticker
停止Ticker时只是简单地把Ticker从系统协程中移除,但并不会关闭通道,以免用户协程读取错误。
func (t *Ticker) Stop(){
stopTimer(&t.r)
}
该函数不需要返回值。
4.3 小结
- NewTicker()创建一个新的Ticker交给系统协程监控
- Stop()通知系统协程删除指定的Ticker