天天看點

golang 系列:定時器 timer

摘要

在 Go 裡有很多種定時器的使用方法,像正常的 Timer、Ticker 對象,以及經常會看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天将會介紹它們的使用方法以及會對它們的底層源碼進行分析,以便于在更好的場景中使用定時器。

Go 裡的定時器

我們先來看看 Timer 對象 以及 time.After 方法,它們都有點偏一次使用的特性。對于 Timer 來說,使用完後還可以再次啟用它,隻需要調用它的 Reset 方法。

// Timer 例子
func main() {
    myTimer := time.NewTimer(time.Second * 5) // 啟動定時器

    for {
        select {
        case <-myTimer.C:
            dosomething()
            myTimer.Reset(time.Second * 5) // 每次使用完後需要人為重置下
        }
    }

    // 不再使用了,結束它
    myTimer.Stop()
}           
// time.After 例子
func main() {
  timeChannel := time.After(10 *  time.Second)
  select {
      case <-timeChannel:
       doSomething()
  }
}           

從上面可以看出來 Timer 允許再次被啟用,而 time.After 傳回的是一個 channel,将不可複用。

而且需要注意的是 time.After 本質上是建立了一個新的 Timer 結構體,隻不過暴露出去的是結構體裡的 channel 字段而已。

是以如果在

for{...}

裡循環使用了 time.After,将會不斷的建立 Timer。如下的使用方法就會帶來性能問題:

// 錯誤的案例 !!!
    func main() {
        for { // for 裡的 time.After 将會不斷的建立 Timer 對象
            select {
                case <-time.After(10 * time.Second):
                doSomething()
            }
        }
    }           

看完了有着 “一次特性” 的定時器,接下來我們來看看按一定時間間隔重複執行任務的定時器:

func main() {
        ticker := time.NewTicker(3 * time.Second)
        for {
            <-ticker.C
            doSomething()
        }
        ticker.Stop()
    }           

這裡的 Ticker 跟 Timer 的不同之處,就在于 Ticker 時間達到後不需要人為調用 Reset 方法,會自動續期。

除了上面的定時器外,Go 裡的 time.Sleep 也起到了類似一次性使用的定時功能。隻不過 time.Sleep 使用了系統調用。而像上面的定時器更多的是靠 Go 的排程行為來實作。

實作原理

當我們通過 NewTimer、NewTicker 等方法建立定時器時,傳回的是一個 Timer 對象。這個對象裡有一個 runtimeTimer 字段的結構體,它在最後會被編譯成 src/runtime/time.go 裡的 timer 結構體。

而這個 timer 結構體就是真正有着定時處理邏輯的結構體。

一開始,timer 會被配置設定到一個全局的 timersBucket 時間桶。每當有 timer 被建立出來時,就會被配置設定到對應的時間桶裡了。

為了不讓所有的 timer 都集中到一個時間桶裡,Go 會建立 64 個這樣的時間桶,然後根據 目前 timer 所在的 Goroutine 的 P 的 id 去哈希到某個桶上:

// assignBucket 将建立好的 timer 關聯到某個桶上
func (t *timer) assignBucket() *timersBucket {
    id := uint8(getg().m.p.ptr().id) % timersLen
    t.tb = &timers[id].timersBucket
    return t.tb
}           

接着 timersBucket 時間桶将會對這些 timer 進行一個最小堆的維護,每次會挑選出時間最快要達到的 timer。

如果挑選出來的 timer 時間還沒到,那就會進行 sleep 休眠。

如果 timer 的時間到了,則執行 timer 上的函數,并且往 timer 的 channel 字段發送資料,以此來通知 timer 所在的 goroutine。

源碼分析

上面提及了下定時器的原理,現在我們來好好看一下定時器 timer 的源碼。

首先,定時器建立時,會調用 startTimer 方法:

func startTimer(t *timer) {
    if raceenabled {
        racerelease(unsafe.Pointer(t))
    }
    // 1.開始把目前的 timer 添加到 時間桶裡
    addtimer(t)
}           

而 addtimer 也就是我們剛剛所說的配置設定到某個桶的動作:

func addtimer(t *timer) {
    tb := t.assignBucket() // 配置設定到某個時間桶裡
    lock(&tb.lock)
    ok := tb.addtimerLocked(t) // 2.添加完後,時間桶執行堆排序,挑選最近的 timer 去執行
    unlock(&tb.lock)
    if !ok {
        badTimer()
    }
}           

addtimerLocked 裡包含了最終的時間處理函數: timerproc,重點分析下:

// 當有新的 timer 添加進來時會觸發一次
// 當休眠到最近的一次時間到來後,也會觸發一次
func timerproc(tb *timersBucket) {
    tb.gp = getg()
    for {
        lock(&tb.lock)
        tb.sleeping = false
        now := nanotime()
        delta := int64(-1)
        for {
            if len(tb.t) == 0 {
                delta = -1
                break
            }
            t := tb.t[0]
            delta = t.when - now
            if delta > 0 { // 定時器的時間還沒到
                break
            }
            ok := true
            if t.period > 0 { // 此處 period > 0,表示是 ticker 類型的定時器,
                // 重置下次調用的時間,幫 ticker 自動續期
                t.when += t.period * (1 + -delta/t.period)
                if !siftdownTimer(tb.t, 0) {
                    ok = false
                }
            } else {
                // “一次性” 定時器,并且時間到了,需要先移除掉,再進行後面的動作
                last := len(tb.t) - 1
                if last > 0 {
                    tb.t[0] = tb.t[last]
                    tb.t[0].i = 0
                }
                tb.t[last] = nil
                tb.t = tb.t[:last]
                if last > 0 {
                    if !siftdownTimer(tb.t, 0) {
                        ok = false
                    }
                }
                t.i = -1 // 标記已清除
            }

            // 執行到這裡表示定時器的時間到了,需要執行對應的函數。
            // 這個函數也就是 sendTime,它會往 timer 的 channel 發送資料,
            // 以通知對應的 goroutine
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&tb.lock)
            if !ok {
                badTimer()
            }
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq)
            lock(&tb.lock)
        }
        if delta < 0 || faketime > 0 { // 沒有定時器需要執行任務,采用 gopark 休眠
            // No timers left - put goroutine to sleep.
            tb.rescheduling = true
            goparkunlock(&tb.lock, waitReasonTimerGoroutineIdle, traceEvGoBlock, 1)
            continue
        }
        // 有 timer 但它的時間還沒到,是以采用 notetsleepg 休眠
        tb.sleeping = true
        tb.sleepUntil = now + delta
        noteclear(&tb.waitnote)
        unlock(&tb.lock)
        notetsleepg(&tb.waitnote, delta)
    }
}           

在上面的代碼中,發現當時間桶裡已經沒有定時器的時候,goroutine 會調用 gopark 去休眠,直到又有新的 timer 添加到時間桶,才重新喚起執行定時器的循環代碼。

另外,當堆排序挑選出來的定時器時間還沒到的話,則會調用 notetsleepg 來休眠,等到休眠時間達到後重新被喚起。

總結

Go 的定時器采用了堆排序來挑選最近的 timer,并且會往 timer 的 channel 字段發送資料,以便通知對應的 goroutine 繼續往下執行。

這就是定時器的基礎原理了,其他流程也隻是休眠喚起的執行罷了,希望此篇能幫助到大家對 Go 定時器的了解!!!