前言
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專家編程》