go定時器--Ticker


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;

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM