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 字段而已。ui

所以若是在 for{...}裏循環使用了 time.After,將會不斷的建立 Timer。以下的使用方法就會帶來性能問題:spa

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

看完了有着 「一次特性」 的定時器,接下來咱們來看看按必定時間間隔重複執行任務的定時器:code

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 結構體。rem

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

一開始,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 定時器的理解!!!


感興趣的朋友能夠搜一搜公衆號「 閱新技術 」,關注更多的推送文章。
能夠的話,就順便點個贊、留個言、分享下,感謝各位支持!
閱新技術,閱讀更多的新知識。
閱新技術

相關文章
相關標籤/搜索