Go語言中的定時器

定時器

Go的定時器是在經(jīng)過一段時間后做一些事情,位于標(biāo)準(zhǔn)庫的time包。主要是time.Timer, time.Ticker和看起來不太明顯的定時器time.Sleep托享。由于從官方文檔中不能清楚地知道這些定時器是怎么實現(xiàn)的俊卤,所以一些人就會認為每個定時器都會有一個自己的goroutine直到時間到了才退出。這是用最簡單的方式來實現(xiàn)的血筑。我們可以用一個小程序來驗證一下绘沉。

package main

import (
    "fmt"
    "os"
    "runtime/debug"
    "time"
)

func main() {
    debug.SetTraceback("system")
    if len(os.Args) == 1 {
        panic("before timers")
    }
    for i := 0; i < 10000; i++ {
        time.AfterFunc(time.Duration(5*time.Second), func() {
            fmt.Println("Hello!")
        })
    }
    panic("after timers")
}

如果沒有參數(shù)這個程序會在定時器之前打印出所有g(shù)oroutine;如果有參數(shù)那么會打印出執(zhí)行了定時器之后的所有g(shù)oroutine。我們需要用到panic不然的話沒有什么簡單的方法來查看運行時的goroutine——它們被runtime.NumGoroutines和runtime.Stack排除了,所以僅剩的方法就是讓程序crash掉氓轰。讓我們看看在生成定時器之前生成了多少個goroutine:

go run afterfunc.go 2>&1 | grep "^goroutine" | wc -l
4

在生成了10K個定時器之后:

go run afterfunc.go 2>&1 | grep "^goroutine" | wc -l
5

哇瑰钮!只多出了一個goroutine!
讓我們接下來看看為什么只單單多出來一個goroutine笨奠。

runtime.timer

所有的定時器都基于相同的數(shù)據(jù)結(jié)構(gòu)——runtime.timer。增加一個新的定時器,你需要實例化runtime.timer并把它傳遞給函數(shù)runtime.startTimer日矫。這里有個time包的例子:

func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}

所以,在這里我們把持續(xù)時間(duration)轉(zhuǎn)換成具體的時間戳when绑榴,定時器應(yīng)該要調(diào)用以c作為參數(shù)的函數(shù)f哪轿。在time包中,函數(shù)f有三種類型:

  • sendTime:發(fā)送當(dāng)前時間到channel或者在發(fā)送被阻塞的情況下丟棄翔怎。被time.Timer和time.Ticker使用窃诉。
  • goFunc:在goroutine中執(zhí)行一些函數(shù)杨耙。被time.AfterFunc使用。
  • goroutineReady:喚醒特定的goroutine飘痛。被runtime.timeSleep使用珊膜。

現(xiàn)在我們已經(jīng)理解了在運行時中定時器是怎樣的了和它們應(yīng)該怎么做的了。接下來讓我們看看運行時是怎么存儲定時器和在時間到了的時候怎么去調(diào)用函數(shù)的宣脉。

runtime.timers

runtime.timers只是一個數(shù)據(jù)結(jié)構(gòu)车柠,堆。當(dāng)你想在一些元素中重復(fù)地去查找極限值(最大或最兴懿)的時候堆就很有用了竹祷。在我們的例子中,極限值就是最接近當(dāng)前時間的when羊苟。是不是很方便塑陵?那么,讓我們看看在最壞的情況下操作定時器的算法復(fù)雜度:

  • 增加新的定時器——O(log(n))
  • 刪除定時器——O(log(n))
  • 生成定時器函數(shù)——O(log(n))

所以蜡励,如果你有一百萬個定時器猿妈,那么操作數(shù)量一般來說會少于1000(log(1kk) ~= 20,但是生成操作會多出來幾個最小值刪除巍虫,因為多個定時器可能在相同的時間達到它們的截止時間) 彭则。這是非常快的占遥,并且所有的工作都發(fā)生在同一個獨立的goroutine中俯抖,所以它不會阻塞。siftupTimer和siftdownTimer函數(shù)是用來維護堆的性質(zhì)的瓦胎。但是數(shù)據(jù)結(jié)構(gòu)不會只為自己工作芬萍,一些東西也需要用到它們。在我們的例子中它只是一個timeproc函數(shù)的goroutine搔啊。它在第一個定時器開始的時候生成柬祠。

runtime.timerproc

如果沒有源碼的話有點難以描述到底發(fā)生了什么,所以這一節(jié)將開始對Go的代碼做一些注釋负芋。代碼是直接從src/runtime/time.go文件直接拷貝過來的并添加了一些注釋漫蛔。

// Add a timer to the heap and start or kick the timerproc if the new timer is
// earlier than any of the others.
// 增加一個定時器到堆中并開始或者刪掉timeproc如果新的定時器早于任何定時器的話
func addtimerLocked(t *timer) {
    // when 必須不能是負值,否則的話 timeproc將會溢出
    // when must never be negative; otherwise timerproc will overflow
    // during its delta calculation and never expire other runtime·timers.
    if t.when < 0 {
        t.when = 1<<63 - 1
    }
    t.i = len(timers.t)
    timers.t = append(timers.t, t)
    // maintain heap invariant
    // 維持堆的性質(zhì)不變
    siftupTimer(t.i)
    // new time is on top
    // 新的時間在堆的頂部了
    if t.i == 0 {
        // siftup moved to top: new earliest deadline.
        // siftup 移到了頂部:新的最早的截止時間
        if timers.sleeping {
            // wake up sleeping goroutine, put to sleep with notetsleepg in timerproc()
            // 喚醒睡眠中的goroutine
            timers.sleeping = false
            notewakeup(&timers.waitnote)
        }
        if timers.rescheduling {
            // run parked goroutine, put to sleep with goparkunlock in timerproc()
            // 運行暫停的goro
            timers.rescheduling = false
            goready(timers.gp, 0)
        }
    }
    if !timers.created {
        // run timerproc() goroutine only once
        // 只運行一次timeproc()
        timers.created = true
        go timerproc()
    }
}

// Timerproc runs the time-driven events.
// It sleeps until the next event in the timers heap.
// If addtimer inserts a new earlier event, addtimerLocked wakes timerproc early.
// Timeproc 運行時間驅(qū)動的事件
// 它會一直睡眠直到下一個事件進入了定時器的堆中
// 如果 addtimer 插入了一個更早的事件旧蛾, addtimerLocked 將會更早地喚醒timeproc
func timerproc() {
    // set timer goroutine
    // 設(shè)置定時器goroutine
    timers.gp = getg()
    // forever loop
    // 死循環(huán)
    for {
        lock(&timers.lock)
        // mark goroutine not sleeping
        // 標(biāo)記goroutine不是在睡眠
        timers.sleeping = false
        now := nanotime()
        delta := int64(-1)
        // iterate over timers in heap starting from [0]
        // 從堆的[0]開始迭代定時器
        for {
            // there are no more timers, exit iterating loop
            // 已經(jīng)沒有其他的定時器了莽龟,退出迭代循環(huán)
            if len(timers.t) == 0 {
                delta = -1
                break
            }
            t := timers.t[0]
            delta = t.when - now
            if delta > 0 {
                break
            }
            // t.period means that it's ticker, so change when and move down
            // in heap to execute it again after t.period.
            // t.period 意味著它是一個ticker,所以改變 when 并移到堆的下方锨天,經(jīng)過t.period再執(zhí)行
            if t.period > 0 {
                // leave in heap but adjust next time to fire
                // 留在堆里但是調(diào)整下次執(zhí)行的時間
                t.when += t.period * (1 + -delta/t.period)
                siftdownTimer(0)
            } else {
                // remove from heap
                // this is just removing from heap operation:
                // - swap first(extremum) with last
                // - set last to nil
                // - maintain heap: move first to its true place with siftdownTimer.
                // 從堆中移除
                // 這是從堆中移除的操作:
                // - 交換第一個和最后一個
                // - 設(shè)置最后一個為nil
                // - 維護堆:用 siftdownTimer把第一個移到正確的位置
                last := len(timers.t) - 1
                if last > 0 {
                    timers.t[0] = timers.t[last]
                    timers.t[0].i = 0
                }
                timers.t[last] = nil
                timers.t = timers.t[:last]
                if last > 0 {
                    siftdownTimer(0)
                }
                // set i to -1, so concurrent deltimer won't do anything to
                // heap.
                // 把 i 設(shè)成 -1毯盈,所以并發(fā)的 deltimer不會對堆做任何事情
                t.i = -1 // mark as removed
            }
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&timers.lock)
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            // call timer function without lock
            // 不用鎖調(diào)用定時器函數(shù)
            f(arg, seq)
            lock(&timers.lock)
        }
        // if delta < 0 - timers is empty, set "rescheduling" and park timers
        // goroutine. It will sleep here until "goready" call in addtimerLocked.
        // 如果 delta < 0 - 定時器為空,設(shè)成 “rescheduling” 并暫停定時器goroutine病袄。它將會一直睡眠直到在 addtimerLocked 中調(diào)用 "goready"
        if delta < 0 || faketime > 0 {
            // No timers left - put goroutine to sleep.
            timers.rescheduling = true
            goparkunlock(&timers.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
            continue
        }
        // At least one timer pending. Sleep until then.
        // If we have some timers in heap, we're sleeping until it's time to
        // spawn soonest of them. notetsleepg will sleep for `delta` period or
        // until notewakeup in addtimerLocked.
        // notetsleepg fills timers.waitnote structure and put goroutine to sleep for some time.
        // timers.waitnote can be used to wakeup this goroutine with notewakeup.
        // 至少還有一個定時器未結(jié)束搂赋,睡眠直到結(jié)束
        // 如果在堆中還有一些定時器赘阀,那么會一直睡眠直到最快的時間到了
        timers.sleeping = true
        noteclear(&timers.waitnote)
        unlock(&timers.lock)
        notetsleepg(&timers.waitnote, delta)
    }
}

這里有兩個變量我覺得應(yīng)該要解釋一下的:
rescheduling和sleeping。它們都是表明goroutine已經(jīng)進入睡眠脑奠,但是卻用到了不同的同步機制基公。

當(dāng)所有當(dāng)前的定時器已經(jīng)被處理,但是在未來一段時間還有需要處理的時候捺信,將會設(shè)置成sleeping。它使用了基于OS的同步欠痴,所以它會調(diào)用一些OS的系統(tǒng)調(diào)用來進入睡眠并喚醒goroutine迄靠,并且系統(tǒng)調(diào)用也意味著它是用OS線程來做這件事的。它使用了 note 結(jié)構(gòu)和下面的函數(shù)來達到同步:

  • noteclear -重置 note 狀態(tài)
  • notetsleepg -使goroutine進入睡眠直到 notewakeup 被調(diào)用或者經(jīng)過一段時間之后(假設(shè)是定時器到下一個定時器之間的時間)喇辽。這個函數(shù)將 timers.waitnote 賦值給“指向定時器goroutine的指針”
  • notewakeup -喚醒調(diào)用notetsleepg的goroutine

如果新的定時器比之前最早的定時器還要早的話掌挚,notewakeup 可能會在 addtimerLocked 中被調(diào)用。

當(dāng)堆中已經(jīng)沒有任何定時器菩咨,也就是沒有事情干的時候吠式,將會設(shè)置成rescheduling。它會利用go 調(diào)度器使用goparkunlock函數(shù)來使goroutine進入睡眠抽米。不像 notetsleepg特占,這樣不會消耗任何的操作系統(tǒng)資源,但同樣的也就不支持“喚醒超時”云茸,因此它不能在 sleeping分支的時候代替 notetsleepg是目。 而當(dāng)一個新的定時器通過 addTimerLocked被加進來的時候,就會調(diào)用 goready函數(shù)來喚醒goroutine标捺。

結(jié)論

我們已經(jīng)了解了Go定時器的底層實現(xiàn)了——運行時既不會每個定時器都一個goroutine懊纳,也不會“免費”使用定時器。理解事情是怎么工作的從而避免過早優(yōu)化是很重要的亡容。而且嗤疯,我們也會學(xué)習(xí)到,閱讀運行時的代碼是相當(dāng)簡單的闺兢,所以你不應(yīng)該害怕茂缚。我希望你能享受這次的閱讀并分享給你的朋友。

備注

這篇文章是翻譯自: How Do They Do It: Timers in Go屋谭,本人翻譯地比較爛阱佛,后面會持續(xù)修改的,歡迎大家提供一些意見戴而。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凑术,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子所意,更是在濱河造成了極大的恐慌淮逊,老刑警劉巖催首,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泄鹏,居然都是意外死亡郎任,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門备籽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舶治,“玉大人,你說我怎么就攤上這事车猬∶姑停” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵珠闰,是天一觀的道長惜浅。 經(jīng)常有香客問我,道長伏嗜,這世上最難降的妖魔是什么坛悉? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮承绸,結(jié)果婚禮上裸影,老公的妹妹穿的比我還像新娘。我一直安慰自己军熏,他們只是感情好空民,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著羞迷,像睡著了一般界轩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衔瓮,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天浊猾,我揣著相機與錄音,去河邊找鬼热鞍。 笑死葫慎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的薇宠。 我是一名探鬼主播偷办,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼澄港!你這毒婦竟也來了椒涯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤回梧,失蹤者是張志新(化名)和其女友劉穎废岂,沒想到半個月后祖搓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡湖苞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年拯欧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片财骨。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡镐作,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出隆箩,到底是詐尸還是另有隱情该贾,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布摘仅,位于F島的核電站靶庙,受9級特大地震影響问畅,放射性物質(zhì)發(fā)生泄漏娃属。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一护姆、第九天 我趴在偏房一處隱蔽的房頂上張望矾端。 院中可真熱鬧,春花似錦卵皂、人聲如沸秩铆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽殴玛。三九已至,卻和暖如春添祸,著一層夾襖步出監(jiān)牢的瞬間滚粟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工刃泌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凡壤,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓耙替,卻偏偏與公主長得像亚侠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俗扇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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