1. 簡介
Ticker是周期性定時器,即周期性的觸發一個事件,通過Ticker本身提供的管道將事件傳遞出去。
Ticker的數據結構與Timer完全一樣
type Ticker struct {
C <- chan Time
r runtimeTimer
}
Ticker對外僅暴露一個channel,指定的時間到來時就往該channel中寫入系統時間,也即一個事件。
在創建Ticker時會指定一個時間,作為事件觸發的周期。這也是Ticker與Timer的最主要的區別。
另外,ticker的英文原意是鍾表的”滴噠”聲,鍾表周期性的產生”滴噠”聲,也即周期性的產生事件
2. 使用場景
2.1 簡單定時任務
有時,我們希望定時執行一個任務,這時就可以使用ticker來完成。
下面代碼演示,每隔1s記錄一次日志:
// TickerDemo 用於演示ticker基礎用法
func TickerDemo() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
log.Println("Ticker tick.")
}
}
for range ticker.C
會持續從管道中獲取事件,收到事件后打印一行日志,如果管道中沒有數據會阻塞等待事件,由於ticker會周期性的向管道中寫入事件,所以上述程序會周期性的打印日志。
2.2 定時聚合任務
有時,我們希望把一些任務打包進行批量處理。比如,公交車發車場景:
- 公交車每隔5分鍾發一班,不管是否已坐滿乘客;
- 已經坐滿乘客情況下,不足五分鍾也發車
代碼示例如下
// TickerLaunch用於演示ticker聚合任務用法
func TickerLaunch() {
ticker := time.NewTicker(5 * time.Minute)
maxPassenger := 30 // 每車最大裝載人數
passengers := make([]string, 0, maxPassenger)
for {
passenger := GetNewPassenger() // 獲取一個新乘客
if passenger != "" {
passengers = append(passengers, passenger)
} else {
time.Sleep(1 * time.Second)
}
select {
case <- ticker.C: // 時間到,發車
Launch(passengers)
passengers = []string{}
default:
if len(passengers) >= maxPassenger { // 時間沒到,車已座滿,發車
Launch(passengers)
passengers = []string{}
}
}
}
}
上面代碼中for循環負責接待乘客上車,並決定是否要發車。每當乘客上車,select語句會先判斷ticker.C中是否有數據,有數據則代表發車時間已到,如果沒有數據,則判斷車是否已坐滿,坐滿后仍然發車。
3. Ticker對外接口
3.1 創建定時器
使用NewTicker()方法就可以創建一個周期性定時器,函數原型如下
func NewTicker(d Duration) *Ticker
其中參數d
即為定時器時間觸發的周期
3.2 停止定時器
使用定時器對外暴露的 Stop 方法就可以停掉一個周期性定時器, 函數原型如下
func (t *Ticker)Stop()
需要注意的是, 該方法會停止計時, 意味著不會向定時器的管道中寫入事件,但管道並不會被關閉。管道在使用完成后,生命周期結束后會自動釋放。
Ticker在使用完后務必要釋放,否則會產生資源泄露,進而會持續消耗CPU資源,最后會把CPU耗盡。
3.3 簡單接口
部分場景下,啟動一個定時器並且永遠不會停止, 比如定時輪詢任務, 此時可以使用一個簡單的Tick函數來獲取定時器的管道, 函數原型如下:
func Tick(d Duration) <-chan Time
這個函數內部實際還是創建一個Ticker,但並不會返回出來,所以沒有手段來停止該Ticker。所以,一定要考慮具體的使用場景。
3.4 錯誤示例
Ticker 用於for循環時, 很容易出現意想不到的資源泄露問題
func WrongTicker() {
for {
select {
case <-time.Tick(1 * time.Second):
log.Printf("Resource leak!")
}
}
}
上面代碼,select每次檢測case語句時都會創建一個定時器,for循環又會不斷地執行select語句,所以系統里會有越來越多的定時器不斷地消耗CPU資源,最終CPU會被耗盡。
4.實現原理
Ticker與之前講的Timer幾乎完全相同,無論數據結構和內部實現機制都相同,唯一不同的是創建方式。
Timer創建時,不指定事件觸發周期,事件觸發后Timer自動銷毀。而Ticker創建時會指定一個事件觸發周期,事件會按照這個周期觸發,如果不顯式停止,定時器永不停止。
4.1 數據結構
Ticker
Ticker數據結構與Timer除名字不同外完全一樣。
源碼包src/time/tick.go:Ticker
定義了其數據結構:
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
r runtimeTimer
}
Ticker只有兩個成員:
- C: 管道,上層應用根據此管道接收事件;
- r: runtime定時器,該定時器即系統管理的定時器,對上層應用不可見;
這里應該按照層次來理解Ticker數據結構,Ticker.C即面向Ticker用戶的,Ticker.r是面向底層的定時器實現。
runtimeTimer
runtimeTimer與Timer一樣
4.2實現原理
4.2.1 創建Ticker
創建Ticker的實現,代碼如下:
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), // Ticker跟Timer的重要區就是提供了period這個參數,據此決定timer是一次性的,還是周期性的
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
NewTicker()只是構造了一個Ticker,然后把Ticker.r通過startTimer()交給系統協程維護。
其中period為事件觸發的周期。
其中sendTime()
方法便是定時器觸發時的動作:
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now():
default:
}
}
sendTime接收一個管道作為參數,其主要任務是向管道中寫入當前時間。
創建Ticker
時生成的管道含有一個緩沖區(make(chan Time, 1))
,但是Ticker觸發的事件卻是周期性的,如果管道中的數據沒有被取走,那么sendTime()也不會阻塞,而是直接退出,帶來的后果是本次事件會丟失。
創建一個Ticker示意圖如下:
4.2.2 停止Ticker
停止Ticker,只是簡單的把Ticker從系統協程中移除。函數主要實現如下:
func (t *Ticker) Stop() {
stopTimer(&t.r)
}
stopTicker()即通知系統協程把該Ticker移除,即不再監控。系統協程只是移除Ticker並不會關閉管道,以避免用戶協程讀取錯誤。
與Timer不同的是,Ticker停止時沒有返回值,即不需要關注返回值,實際上返回值也沒啥用途。
停止一個Ticker示意圖如下:
Ticker沒有重置接口,也即Ticker創建后不能通過重置修改周期。
需要格外注意的是Ticker用完后必須主動停止,否則會產生資源泄露,會持續消耗CPU資源。
總結
Ticker相關內容總結如下:
- 使用time.NewTicker()來創建一個定時器;
- 使用Stop()來停止一個定時器;
- 定時器使用完畢要釋放,否則會產生資源泄露;
- NewTicker()創建一個新的Ticker交給系統協程監控;
- Stop()通知系統協程刪除指定的Ticker;