Go中的定时器(timer/ticker)


前言

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专家编程》

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM