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