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。

定时器的创建流程:

  1. 根据ProcessID%64来选择对应的timersBucket。
  2. timersBucket中的切片中保存着timer的指针,并且新加入的timer是按照触发时间排序的小头堆,需要进行堆排序。

timerproc是系统协程的具体实现,在首次创建定时器创建并启动,一旦启动永不销毁。如果timersBucket中由定时器,取出堆顶定时器,计算睡眠时间,然后进入睡眠,醒来后触发事件。

某个timer事件触发后,根据其是否是周期定时器来决定将其删除还是重新加入堆中。

如果堆中没有事件需要触发,则系统协程进入暂停态,直接新的timer加入才会被唤醒。