Go提供两种定时器
- 一次性定时器:定时器只执行一次,结束便停止
- 周期性定时器:定时器周期性进行计时,除非主动停止。
Timer
Timer是单一事件定时器,经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。 Timer对外只暴露一个channel,指定的时间到来时就往该channel中写入系统时间。
通过方法func NewTimer(d Duration) *Timer指定一个时间创建一个Timer,Timer一经创建便开始计时,不需要额外的启动命令。
不过Timer创建之后可以随时停止,通过:
func (t *Timer) Stop() bool这个函数的返回值,表示停止定时器的时候,定时器有没有超时。false表示在定时器超时之后才停止。
通过下面的方法可以重置定时器,重置定时器的本质就是先从系统守护协程移除该定时器,重新设置时间后,再添加回去。
func (t *Timer) Reset(d Duration) bool简单接口
After(d Duration):创建一个定时器,并返回定时器的管道。AfterFunc(d Duration, f func()) * Timer:在指定时间到来后执行函数f。
实现原理
在src/time/sleep.go:Timer中定义了Timer的数据结构:
type Timer struct {
C <- chan Time
r runtimeTimer
}- C:管道,上层应用根据此管道接受事件
- r:runtime定时器,该定时器即系统管理的定时器,对上层应用不可见。
C是面向Timer的用户的,r是面向底层的实现的。
创建一个Timer的实质是把一个定时任务交给专门的协程进行监控,这个任务的载体便是runtimeTimer变量,通过设置runtimeTimer过期后的行为来达到定时的目的,源码包src/time/sleep.go:runtimeTimer定义了其数据结构:
type runtimeTimer struct {
tb uintptr //存储当前定时器的数组地址
i int //存储当前定时器的数组下标
when int64 //当前定时器触发时间
period int64 //当前定时器触发间隔
f func(interface{}, uintptr) //定时器触发时执行的函数
arg interface{} //定时器触发时执行函数传递的参数一
seq uintptr //定时器触发时执行函数传递的参数二
}NewTimer的实现很简单:
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
}创建了Timer对象,并将其r交给系统协程维护,然后再超时时回调sendTime函数,其实就是往管道写入当前时间:
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now():
default:
}
}
停止Timer就是将对应r从系统协程中移除:

重置Timer是先删除再添加:

使用Reset重置最好作用于已经停掉的Timer或者已经触发的Timer,按照这个约定其返回值将总是返回false,若不按照此约定,可能遇到Reset()和Timer同时触发同时执行的情况,此时有可能收到两个事件,从而对应用程序造成一些负面影响。
Ticker
Ticker是周期性定时器,即周期性触发一个事件,通过Ticker本身提供的管道将事件传递出去。其数据结构定义在src/time/tick.go:Ticker中:
type Ticker struct {
C <- chan Time
r runtimeTimer
}数据结构与Timer一致。其实两者最主要的区别也就是Ticker会周期性触发而已。
实现原理
实现的原理与Timer的基本一致,不过Ticker没有重置接口,并且Ticker使用完成之后需要主动停止,否则会产生资源泄露,会持续消耗CPU资源。
timer
Timer与Ticker的内部实现机制完全相同,两者包含一个runtimeTimer类型的成员,并且由系统协程管理,现在就来学习系统协程是如何管理这些定时器的。
runtimeTimer是系统协程所维护的对象,此类型是time包的名称,在runtime包中,这个类型叫做timer。数据结构如下所示:
type timer struct {
tb *timersBucket //当前定时器寄存于系统timer堆的地址
i int //当前定时器寄存于系统timer堆的下标
when int64 //当前定时器触发时间
period int64 //当前定时器触发间隔
f func(interface{}, uintptr) //定时器触发时执行的函数
arg interface{} //定时器触发时执行函数传递的参数一
seq uintptr //定时器触发时执行函数传递的参数二
}timersBucket是系统协程存储timer的容器,里面有个切片来存储timer,而i便是timer所在切片的下标。
timersBucket的数据结构如下所示:
type timersBucket struct {
lock mutex
gp *g //处理堆中事件的协程
created bool // 事件处理协程是否已创建
sleep bool //事件处理协程是否在睡眠
rescheduling bool //事件处理协程是否已暂停
sleepUntil int64 //事件处理协程睡眠事件
waitnote note //事件处理协程睡眠事件(据此唤醒协程)
t []*timer //定时器切片
}
系统协程负责计时并维护其中的多个timer,一个timersBucket包含一个协程协程。当系统中定时器非常多时,会创建多个timersBucket,也就有多个系统协程来处理定时器。
Go在是实现时预留了64个timersBucket,每当协程创建定时器时,使用协程所属的ProcessID%64来计算定时器存入的timersBucket。
定时器的创建流程:
- 根据ProcessID%64来选择对应的timersBucket。
- timersBucket中的切片中保存着timer的指针,并且新加入的timer是按照触发时间排序的小头堆,需要进行堆排序。
timerproc是系统协程的具体实现,在首次创建定时器创建并启动,一旦启动永不销毁。如果timersBucket中由定时器,取出堆顶定时器,计算睡眠时间,然后进入睡眠,醒来后触发事件。
某个timer事件触发后,根据其是否是周期定时器来决定将其删除还是重新加入堆中。
如果堆中没有事件需要触发,则系统协程进入暂停态,直接新的timer加入才会被唤醒。
