前言
go中的定时器包含了两种,一种是一次性的定时器Timer,另外一种是周期性的定时器Ticker。
Timer
先看一下Timer是怎么使用的。Timer通常有两种使用方式,一种是显式创建一个定时器,一个是使用匿名定时器:
func main() { modeOne() moddTwo() } func modeOne() { timer := time.NewTimer(time.Second * 5) <- timer.C fmt.Println("mode one: Time out!") } func moddTwo() { select { case <-time.After(time.Second * 5): fmt.Println("mode two: Time out!") } }
开始的时候可能很迷,为什么模式2就可以作为定时器了呢。了解定时器的结构就很清楚了。下面是定时器的结构体定义:
type Timer struct { C <-chan Time // 抛出来的channel,给上层系统使用,实现定时 r runtimeTimer // 给系统管理使用的定时器,系统通过该字段确定定时器是否到时,如果到时,调用对应的函数向C中推送当前时间。 }
方式二中的定时器使用方式是通过After函数构造了一个匿名定时器,并抛出来管道C。总之,两种方式大同小异,都是通过管道C来实现的,定时到了以后,C中有值,则进行相应的操作。
如果需要停止定时器,可以调用Stop方法,该方法把runtimeTimer从堆中删除。时间到了以后想要调用某个函数,可以直接使用time.AfterFunc方法。
那么定时器是如何实现的呢?首先看一下定时器的构造:
func NewTimer(d Duration) *Timer { c := make(chan Time, 1) t := &Timer{ C: c, // 信道 r: runtimeTimer{ when: when(d), // 触发时间 f: sendTime, // 时间到了之后的调用函数 arg: c, // 调用sendTime时的入参 }, } startTimer(&t.r) // 把定时器的r字段放入由定时器维护协程维护的堆中 return t }
从上面的构造函数中可以大概看出定时器的工作流程,这里面最重要的是runtimeTimer。构造定时器的时候会把runtimeTimer放入由定时器维护协程维护的堆中,当时间到了之后,维护协程把r从堆中移除,并调用r的sendTime函数,sendTime的入参是定时器的信道C。可以推断,sendTime中执行的逻辑应该是向信道C中推送时间,通知上游系统时间到了,而事实正是如此:
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: } }
可能你会有疑问,为什么这里要用到select,直接往c中放值不久好了吗,而且default分支有什么作用?这里就是定时器设计巧妙的地方,前面讲到go中的定时器包含了一次性定时器和周期定时器,而sendTime是两种定时器共用的。其实Ticker和Timer基本上没有什么差别,实现原理是一样的,结构体字段也是一样的,至少runtimeTimer在构造的时候传入的参数有细微的差别。在Ticker时间到了之后,由于不确定信道C中的内容是否被取走,所以为了sendTime不阻塞,这个时候会走default分支,也就是会丢失一个信号。
Ticker
先看一个Ticker的使用示例:
func main() { tickerDemo() } func tickerDemo() { ticker := time.NewTicker(time.Second) defer ticker.Stop() for range ticker.C { fmt.Println("Time Out!") } }
Ticker结构体的定义和构造函数如下所示,可以看到与Timer基本一致:
type Ticker struct { C <-chan Time // The channel on which the ticks are delivered. r runtimeTimer } func NewTicker(d Duration) *Ticker { if d <= 0 { panic(errors.New("non-positive interval for NewTicker")) } // Give the channel a 1-element time buffer. // If the client falls behind while reading, we drop ticks // on the floor until the client catches up. c := make(chan Time, 1) t := &Ticker{ C: c, r: runtimeTimer{ when: when(d), period: int64(d), // 与一次性定时器不一样的地方,这个参数决定了定时器是周期的 f: sendTime, arg: c, }, } startTimer(&t.r) return t }
周期性定时器到期了之后同样是执行sendTime方法,这个上面已经描述过了。细心的你肯定注意到了,在tickerDemo中有一个defer去停止ticker,为什么要这么做呢?前面分析的时候讲到,创建定时器就是把定时器的runtimeTimer放到由维护协程维护的堆中,一次性定时器到期后,会从堆中删除,如果没有到期则调用Stop方法实现删除。但是,周期性定时器是不会执行删除动作的,所以如果项目里面持续创建多个周期性定时器并没有stop的话,会导致堆越来越大,从而引起资源泄露。
参考:任洪彩.《GO专家编程》