Go的定時器之Time.Ticker

一、引子

面試官問了一道題:每秒鐘調用一次proc並保證程序不退出。

package main

func main() {

}

func proc() {
    panic("ok")
}

這道題考察的知識點主要有:

  1. 定時執行任務
  2. 捕獲 panic 錯誤

這裏主要學習、瞭解 Time.Ticker 的實現,其源代碼基於 Go 1.17.9 版本,主要在 src/time/tick.go 文件中,包含了一個結構體和四個函數。

二、Time.Ticker

Ticker 是一個週期觸發定時的計時器,它會按照一個時間間隔往 channel 發送系統當前時間,而 channel 的接收者可以以固定的時間間隔從 channel 中讀取事件。

2.1 結構體

type Ticker struct {
    C <-chan Time // The channel on which the ticks are delivered.
    r runtimeTimer
}

//注:該結構體在src/time/sleep.go中
type runtimeTimer struct {
    pp       uintptr
    when     int64
    period   int64
    f        func(any, uintptr) // NOTE: must not be closure
    arg      any
    seq      uintptr
    nextwhen int64
    status   uint32
}

可以看到這個結構體包含了一個只讀的通道 C,並每隔一段時間向其傳遞"tick"。

2.2 NewTicker()

NewTicker() 主要包含兩步:

  1. 創建一個 Ticker,主要包括其中的 C 屬性和 r 屬性。r 屬性是 runtimeTimer 類型。

  2. 調用 startTimer 函數,啓動 Ticker

如果 d <= 0panic

func NewTicker(d Duration) *Ticker {
    if d <= 0 {
        panic(errors.New("non-positive interval for NewTicker"))
    }
	
    c := make(chan Time, 1)
    t := &Ticker{
        C: c,
        r: runtimeTimer{
            when:   when(d),
            period: int64(d),
            f:      sendTime, // f表示一個函數調用,這裏的sendTime表示d時間到達時向Timer.C發送當前的時間
            arg:    c, // arg表示在調用f的時候把參數arg傳遞給f,c就是用來接受sendTime發送時間的
        },
    }
    startTimer(&t.r)
    return t
}

這裏主要關注 fargstartTimer(&t.r)

  • f 表示一個函數調用,這裏的 sendTime 表示 d 時間到達時,向 Timer.C 發送當前的時間;

  • arg 表示在調用 f 的時候把參數 arg 傳遞給 fc 就是用來接受 sendTime 發送時間的。

其中 f 對應的函數爲:

func sendTime(c any, seq uintptr) {
    select {
    case c.(chan Time) <- Now():
    default:
    }
}

ticker 對象構造好後,就調用了 startTimer 函數,startTimer 具體的函數定義在 runtime/time.go 中

// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
    if raceenabled {
        racerelease(unsafe.Pointer(t))
    }
    addtimer(t)
}

裏面實際調用了 addtimer() 函數。

func addtimer(t *timer) {
    // when must be positive. A negative value will cause runtimer to
    // overflow during its delta calculation and never expire other runtime
    // timers. Zero will cause checkTimers to fail to notice the timer.
    if t.when <= 0 {
        throw("timer when must be positive")
    }
    if t.period < 0 {
        throw("timer period must be non-negative")
    }
    if t.status != timerNoStatus {
        throw("addtimer called with initialized timer")
    }
    t.status = timerWaiting

    when := t.when

    // Disable preemption while using pp to avoid changing another P's heap.
    // 禁用p被搶佔去避免去改變其他p的堆棧
    mp := acquirem()

    pp := getg().m.p.ptr() // 獲取當前p
    lock(&pp.timersLock)
    cleantimers(pp)       // 清除timers
    doaddtimer(pp, t)     // 添加timer到當前p的堆上,在鎖中執行
    unlock(&pp.timersLock) 

    wakeNetPoller(when)   // 添加到Netpoller

    releasem(mp)
}

addtimer 就是將 timer 加到當前執行 ptimers 數組裏面去,調用 wakeNetPoller 方法喚醒網絡輪詢器中休眠的線程,檢查計時器被喚醒的時間(when)是否在當前輪詢預期運行的時間內,若是,就喚醒。

2.3 stop()

Stop 關閉一個 Ticker,但不會關閉通道 t.C,防止讀取通道發生錯誤。

func (t *Ticker) Stop() {
    stopTimer(&t.r)
}

stopTimer 具體的函數定義也是在 runtime/time.go 中,實際調用了 deltimer() 函數。

func stopTimer(t *timer) bool {
    return deltimer(t)
}

func deltimer(t *timer) bool {
    for {
        switch s := atomic.Load(&t.status); s {
        case timerWaiting, timerModifiedLater:
            // Prevent preemption while the timer is in timerModifying.
            // This could lead to a self-deadlock. See #38070.
            mp := acquirem()
            if atomic.Cas(&t.status, s, timerModifying) {
                // Must fetch t.pp before changing status,
                // as cleantimers in another goroutine
                // can clear t.pp of a timerDeleted timer.
                tpp := t.pp.ptr()
                if !atomic.Cas(&t.status, timerModifying, timerDeleted) {
                    badTimer()
                }
                releasem(mp)
                atomic.Xadd(&tpp.deletedTimers, 1)
                // Timer was not yet run.
                return true
            } else {
                releasem(mp)
            }
        case timerModifiedEarlier:
            // Prevent preemption while the timer is in timerModifying.
            // This could lead to a self-deadlock. See #38070.
            mp := acquirem()
            if atomic.Cas(&t.status, s, timerModifying) {
                // Must fetch t.pp before setting status
                // to timerDeleted.
                tpp := t.pp.ptr()
                if !atomic.Cas(&t.status, timerModifying, timerDeleted) {
                    badTimer()
                }
                releasem(mp)
                atomic.Xadd(&tpp.deletedTimers, 1)
                // Timer was not yet run.
                return true
            } else {
                releasem(mp)
            }
        case timerDeleted, timerRemoving, timerRemoved:
            // Timer was already run.
            return false
        case timerRunning, timerMoving:
            // The timer is being run or moved, by a different P.
            // Wait for it to complete.
            osyield()
        case timerNoStatus:
            // Removing timer that was never added or
            // has already been run. Also see issue 21874.
            return false
        case timerModifying:
            // Simultaneous calls to deltimer and modtimer.
            // Wait for the other call to complete.
            osyield()
        default:
            badTimer()
        }
    }
}

簡單來說就是修改 timer 的狀態,先修改爲“已修改”,再修改爲“刪除”。

2.4 Reset()

Reset() 調用 modTimer() 修改時間,接下來的激活將在新 d 後。

func (t *Ticker) Reset(d Duration) {
    if d <= 0 {
        panic("non-positive interval for Ticker.Reset")
    }
    if t.r.f == nil {
        panic("time: Reset called on uninitialized Ticker")
    }
    modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
}

2.5 Tick()

返回 tickerchannel

func Tick(d Duration) <-chan Time {
    if d <= 0 {
        return nil
    }
    return NewTicker(d).C
}

三、小結

  1. Go 的定時器實質是單向通道,time.Ticker 結構體類型中有一個time.Time 類型的單向 channel
  2. ticker 創建完之後,不是馬上就有一個 tick,第一個 tick 在 x 秒之後。
  3. time.NewTicker 定時觸發執行任務,當下一次執行到來而當前任務還沒有執行結束時,會等待當前任務執行完畢後再執行下一次任務。
  4. Stop 不會停止定時器。這是因爲 Stop 會停止 Timer,停止後,Timer 不會再被髮送,但是 Stop 不會關閉通道,防止讀取通道發生錯誤。如果想停止定時器,只能讓 go 程序自動結束。

回到開頭的問題,怎麼實現每秒執行一次proc並保證程序不退出,可以使用 time.Ticker 實現定時器的功能,使用 recover() 函數捕獲 panic 錯誤。參考答案如下:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        t := time.NewTicker(time.Second * 1)
        for {
            select {
            case <-t.C:
                go func() {
                    defer func() {
                        if err := recover(); err != nil {
                            fmt.Println("recover", err)
                        }
                    }()
                }()

                proc()
            }
        }
    }()

    select {}
}

func proc() {
    panic("ok")
}


參考鏈接:深入解析go Timer 和Ticker實現原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章