Go語言定時器實現原理及作用

2020-07-16 10:05:16
對於任何一個正在執行的應用,如何獲取準確的絕對時間都非常重要,但是在一個分散式系統中我們很難保證各個節點上絕對時間的一致性,哪怕通過 NTP 這種標準的對時協定也只能把時間的誤差控制在毫秒級,所以相對時間在一個分散式系統中顯得更為重要,在接下來的講解中我們將會介紹一下Go語言中的定時器以及它在並行程式設計中起到什麼樣的作用。

絕對時間一定不會是完全準確的,它對於一個執行中的分散式系統其實沒有太多指導意義,但是由於相對時間的計算不依賴於外部的系統,所以它的計算可以做的比較準確,首先介紹一下Go語言中用於計算相對時間的定時器的實現原理。

結構

timer 就是Go語言定時器的內部表示,每一個 timer 其實都儲存在堆中,tb 就是用於儲存當前定時器的桶,而 i 是當前定時器在堆中的索引,我們可以通過這兩個變數找到當前定時器在堆中的位置:

type timer struct {
    tb *timersBucket
    i  int

    when   int64
    period int64
    f      func(interface{}, uintptr)
    arg    interface{}
    seq    uintptr
}

when 表示當前定時器(Timer)被喚醒的時間,而 period 表示兩次被喚醒的間隔,每當定時器被喚醒時都會呼叫 f(args, now) 函數並傳入 args 和當前時間作為引數。

然而這裡的 timer 作為一個私有結構體其實只是定時器的執行時表示,time 包對外暴露的定時器使用了如下所示的結構體:

type Timer struct {
    C <-chan Time
    r runtimeTimer
}

Timer 定時器必須通過 NewTimer 或者 AfterFunc 函數進行建立,其中的 runtimeTimer 其實就是上面介紹的 timer 結構體,當定時器失效時,失效的時間就會被傳送給當前定時器持有的 Channel C,訂閱管道中訊息的 Goroutine 就會收到當前定時器失效的時間。

在 time 包中,除了 timer 和 Timer 兩個分別用於表示執行時定時器和對外暴露的 API 之外,timersBucket 這個用於儲存定時器的結構體也非常重要,它會儲存一個處理器上的全部定時器,不過如果當前機器的核數超過了 64 核,也就是機器上的處理器 P 的個數超過了 64 個,多個處理器上的定時器就可能儲存在同一個桶中:

type timersBucket struct {
    lock         mutex
    gp           *g
    created      bool
    sleeping     bool
    rescheduling bool
    sleepUntil   int64
    waitnote     note
    t            []*timer
}

每一個 timersBucket 中的 t 就是用於儲存定時器指標的切片,每一個執行的Go語言程式都會在記憶體中儲存著 64 個桶,這些桶中都儲存定時器的資訊: