Go-Timer源碼解讀

image

前言

在初學(xué)Go定時任務(wù)之時,腦海中始終有一個問題在徘徊,究竟是每個任務(wù)都有一個goroutine去監(jiān)控,還是多個任務(wù)處于同一個隊列,讓同一個goroutine去輪詢檢查。這里大家可以帶著這個問題去進行接下來的閱讀。

Example

先來看一個簡單的例子,這里我選擇了NewTicker去進行測試,它和NewTimer唯一的區(qū)別是:前者定時循環(huán)執(zhí)行,后者只會執(zhí)行一次。

func main() {
    t := time.NewTicker(5 * time.Second)
    for {
        select {
        case <-t.C:
            log.Println("xxxxxxxxxx")
        }
    }
}

這里會每隔5秒就會執(zhí)行一次打印,但這個究竟是怎么實現(xiàn)的,咱們一步步的去探索。

源碼部分

NewTicker

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,
            arg:    c,
        },
    }
    startTimer(&t.r)
    return t
}

簡單的看一下第一個函數(shù),不算太復(fù)雜,進行簡單的異常判斷,創(chuàng)建一個緩沖為1的channel,并構(gòu)建核心的結(jié)構(gòu)體runtimeTimer。下面我們關(guān)注一下這個結(jié)構(gòu)體的幾個屬性。(由于runtimeTimertimer底層結(jié)構(gòu)一致,我這里截取timer結(jié)構(gòu)體的源碼進行解釋一下相關(guān)屬性)

type timer struct {
    tb *timersBucket // 在哪個桶中存在,這里是根據(jù)goroutine所屬的p確定的
    i  int           // 在堆結(jié)構(gòu)中的索引位置
    when   int64 // 啥時候去執(zhí)行函數(shù)f
    period int64 // 間隔多久去執(zhí)行函數(shù)f,該值為0時表示只會執(zhí)行一次函數(shù)f
    f      func(interface{}, uintptr)
    arg    interface{} // 函數(shù)f的第一個參數(shù)
    seq    uintptr // 函數(shù)f的第二個參數(shù)
}

startTimer

func startTimer(*runtimeTimer)

接著看startTimer這個函數(shù),初學(xué)者看這段源碼時可能會覺得奇怪,因為它根本沒有body。其實類似的情況并不少見,像這種沒有方法體的大多都會在runtime包給其提供實現(xiàn)。如下所示:

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

注意一下這個方法上面有一句注釋//go:linkname startTimer time.startTimer,這句注釋可不是一個無用的注釋,簡單的來說go:linkname這個指令告訴編譯器為當(dāng)前源文件中私有函數(shù)或者變量在編譯時鏈接到指定的方法或變量。所以在這里大家可以把這個理解為runtime.startTimertime.startTimer的具體實現(xiàn)。

addtimer

func addtimer(t *timer) {
    tb := t.assignBucket()
    lock(&tb.lock)
    ok := tb.addtimerLocked(t)
    unlock(&tb.lock)
    if !ok {
        badTimer()
    }
}

這個函數(shù)主要干了兩件事:

  • 獲取這個timer屬于哪個bucket,這里是根據(jù)goroutine所屬的p的id來進行計算。
  • timer其添加到對應(yīng)的bucket中。

這里可以保證在大部分情況下同一個p上創(chuàng)建的timer可以放到同一個bucket中,除非你的機器CPU核數(shù)超過了64個。每個核上維護著一個隊列,在某種程度上也是提升了定時任務(wù)的性能。

addtimerLocked

func (tb *timersBucket) addtimerLocked(t *timer) bool {
    // 保證when的值是正數(shù)
    if t.when < 0 {
        t.when = 1<<63 - 1
    }
    t.i = len(tb.t)
    tb.t = append(tb.t, t)
    // 根據(jù)when的值去調(diào)整堆中的順序
    if !siftupTimer(tb.t, t.i) {
        return false
    }
    if t.i == 0 {
    
        if !tb.created {
            tb.created = true
            // 進行對當(dāng)前bucket監(jiān)控的goroutine的創(chuàng)建
            go timerproc(tb)
        }
    }
    return true
}

這里存儲timer的數(shù)據(jù)結(jié)構(gòu)是四叉樹。相同的數(shù)據(jù)而言,四叉樹比二叉樹的深度要低,查詢時效率要高一點。在實現(xiàn)定時器時為啥要選擇四叉樹而不是二叉樹,大家可以參考一下這篇文章定時器:4叉堆與2叉堆的效率比較。

這個方法大概分三步:

  • tb.t = append(tb.t, t),將其插入到數(shù)組的最后一個。
  • siftupTimer(tb.t, t.i),將最后一個timer和它的parent進行比較,由于這里的數(shù)據(jù)結(jié)構(gòu)是四叉樹,所以它的parent計算公式為p := (i - 1) / 4 // parent,如果比parent小,則進行交換。這里會遞歸執(zhí)行,直到取到符合timer的位置為止。
  • 判斷監(jiān)控該bucket的goroutine是否已經(jīng)創(chuàng)建,如果沒有,則進行創(chuàng)建。

timerproc

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
            }
            // 由于這里是最小堆,取出堆頂元素也就是最靠近執(zhí)行時間的那個timer
            t := tb.t[0]
            delta = t.when - now
            if delta > 0 {
                break
            }
            ok := true
            if t.period > 0 {
                // 這里表示這個timer是一個定時輪詢的任務(wù),所以加上執(zhí)行周期重新
                // 調(diào)整在堆中的位置
                // leave in heap but adjust next time to fire
                t.when += t.period * (1 + -delta/t.period)
                // 調(diào)整該timer在堆中的位置
                if !siftdownTimer(tb.t, 0) {
                    ok = false
                }
            } else {
                // 只執(zhí)行一次的任務(wù),執(zhí)行后直接從堆中移除
                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 // mark as removed
            }
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&tb.lock)
            if !ok {
                badTimer()
            }
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            // 執(zhí)行timer結(jié)構(gòu)中的f函數(shù)
            f(arg, seq)
            lock(&tb.lock)
        }
        if delta < 0 || faketime > 0 {
            // No timers left - put goroutine to sleep.
            tb.rescheduling = true
            goparkunlock(&tb.lock, waitReasonTimerGoroutineIdle, traceEvGoBlock, 1)
            continue
        }
        // At least one timer pending. Sleep until then.
        tb.sleeping = true
        tb.sleepUntil = now + delta
        noteclear(&tb.waitnote)
        unlock(&tb.lock)
        notetsleepg(&tb.waitnote, delta)
    }
}

執(zhí)行流程:

  1. 由于是最小堆,從堆頂取出的timer就是最近一個將要執(zhí)行的任務(wù),與當(dāng)前時間進行對比,判斷是否已經(jīng)到了執(zhí)行任務(wù)的時間。
  2. 如果是定時輪詢?nèi)蝿?wù),取出來做好記錄后需要調(diào)整該timer的屬性when的值,并在堆中進行重新排序。方便下一次的執(zhí)行。
  3. 如果是執(zhí)行一次的任務(wù),取出來做好記錄后需要從堆中進行移除。
  4. 執(zhí)行特定的函數(shù),例如sendTimegoFunc等等函數(shù)。

總結(jié)

通過上面的了解咱們可以完美解決咱們在文章開始的時候提出的那個問題,究竟開了多少個goroutine去維護咱們的定時任務(wù)隊列?答案是:比如你的機器有n個CPU,那么就會有n個bucket,同樣就會有n個goroutine去監(jiān)控這些bucket,由于存儲結(jié)構(gòu)采用的是最小堆,這里咱們也不用輪詢檢查,只用檢查堆中的第一個元素即可。當(dāng)然最后得出的結(jié)論并不屬于咱們上面兩個猜測的其中任何一個。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內(nèi)容