在 Go 裏有不少種定時器的使用方法,像常規的 Timer、Ticker 對象,以及常常會看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法,今天將會介紹它們的使用方法以及會對它們的底層源碼進行分析,以便於在更好的場景中使用定時器。函數
咱們先來看看 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 定時器的理解!!!
感興趣的朋友能夠搜一搜公衆號「 閱新技術 」,關注更多的推送文章。
能夠的話,就順便點個贊、留個言、分享下,感謝各位支持!
閱新技術,閱讀更多的新知識。