摘要
在 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 定時器的了解!!!