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