最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go timer如何調(diào)度

 更新時(shí)間:2021年06月09日 09:30:56   作者:haohongfan  
本篇文章剖析下 Go 定時(shí)器的相關(guān)內(nèi)容。定時(shí)器不管是業(yè)務(wù)開發(fā),還是基礎(chǔ)架構(gòu)開發(fā),都是繞不過去的存在,由此可見定時(shí)器的重要程度,感興趣的可以了解一下

本篇文章剖析下 Go 定時(shí)器的相關(guān)內(nèi)容。定時(shí)器不管是業(yè)務(wù)開發(fā),還是基礎(chǔ)架構(gòu)開發(fā),都是繞不過去的存在,由此可見定時(shí)器的重要程度。

我們不管用 NewTimer, timer.After,還是 timer.AfterFun 來初始化一個(gè) timer, 這個(gè) timer 最終都會(huì)加入到一個(gè)全局 timer 堆中,由 Go runtime 統(tǒng)一管理。

全局的 timer 堆也經(jīng)歷過三個(gè)階段的重要升級(jí)。

  • Go 1.9 版本之前,所有的計(jì)時(shí)器由全局唯一的四叉堆維護(hù),協(xié)程間競爭激烈。
  • Go 1.10 - 1.13,全局使用 64 個(gè)四叉堆維護(hù)全部的計(jì)時(shí)器,沒有本質(zhì)解決 1.9 版本之前的問題
  • Go 1.14 版本之后,每個(gè) P 單獨(dú)維護(hù)一個(gè)四叉堆。

Go 1.14 以后的 timer 性能得到了質(zhì)的飛升,不過伴隨而來的是 timer 成了 Go 里面最復(fù)雜、最難梳理的數(shù)據(jù)結(jié)構(gòu)。本文不會(huì)詳細(xì)分析每一個(gè)細(xì)節(jié),我們從大體來了解 Go timer 的工作原理。

1. 使用場景

Go timer 在我們代碼中會(huì)經(jīng)常遇到。

場景1:RPC 調(diào)用的防超時(shí)處理(下面代碼節(jié)選 dubbogo)

func (c *Client) Request(request *remoting.Request, timeout time.Duration, response *remoting.PendingResponse) error {
    _, session, err := c.selectSession(c.addr)
    // .. 省略
    if totalLen, sendLen, err = c.transfer(session, request, timeout); err != nil {
        if sendLen != 0 && totalLen != sendLen {
          // .. 省略
        }
        return perrors.WithStack(err)
    }

    // .. 省略
    select {
    case <-getty.GetTimeWheel().After(timeout):
        return perrors.WithStack(errClientReadTimeout)
    case <-response.Done:
        err = response.Err
    }
    return perrors.WithStack(err)
}

場景2:Context 的超時(shí)處理

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    go doSomething()
    
    select {
    case <-ctx.Done():
        fmt.Println("main", ctx.Err())
    }
}

2. 圖解源碼

2.1 四叉堆原理

timer 的全局堆是一個(gè)四叉堆,特別是 Go 1.14 之后每個(gè) P 都會(huì)維護(hù)著一個(gè)四叉堆,減少了 Goroutine 之間的并發(fā)問題,提升了 timer 了性能。

四叉堆其實(shí)就是四叉樹,Go timer 是如何維護(hù)四叉堆的呢?

  • Go runtime 調(diào)度 timer 時(shí),觸發(fā)時(shí)間更早的 timer,要減少其查詢次數(shù),盡快被觸發(fā)。所以四叉樹的父節(jié)點(diǎn)的觸發(fā)時(shí)間是一定小于子節(jié)點(diǎn)的。
  • 四叉樹顧名思義最多有四個(gè)子節(jié)點(diǎn),為了兼顧四叉樹插、刪除、重排速度,所以四個(gè)兄弟節(jié)點(diǎn)間并不要求其按觸發(fā)早晚排序。

這里用兩張動(dòng)圖簡單演示下 timer 的插入和刪除

把 timer 插入堆

把 timer 從堆中刪除

2.2 timer 是如何被調(diào)度的?

調(diào)用 NewTimer,timer.After, timer.AfterFunc 生產(chǎn) timer, 加入對(duì)應(yīng)的 P 的堆上。

調(diào)用 timer.Stop, timer.Reset 改變對(duì)應(yīng)的 timer 的狀態(tài)。

GMP 在調(diào)度周期內(nèi)中會(huì)調(diào)用 checkTimers ,遍歷該 P 的 timer 堆上的元素,根據(jù)對(duì)應(yīng) timer 的狀態(tài)執(zhí)行真的操作。

2.3 timer 是如何加入到 timer 堆上的?

把 timer 加入調(diào)度總共有下面幾種方式:

  • 通過 NewTimer, time.After, timer.AfterFunc 初始化 timer 后,相關(guān) timer 就會(huì)被放入到對(duì)應(yīng) p 的 timer 堆上。
  • timer 已經(jīng)被標(biāo)記為 timerRemoved,調(diào)用了 timer.Reset(d),這個(gè) timer 也會(huì)重新被加入到 p 的 timer 堆上
  • timer 還沒到需要被執(zhí)行的時(shí)間,被調(diào)用了 timer.Reset(d),這個(gè) timer 會(huì)被 GMP 調(diào)度探測到,先將該 timer 從 timer 堆上刪除,然后重新加入到 timer 堆上
  • STW 時(shí),runtime 會(huì)釋放不再使用的 p 的資源,p.destroy()->timer.moveTimers,將不再被使用的 p 的 timers 上有效的 timer(狀態(tài)是:timerWaiting,timerModifiedEarlier,timerModifiedLater) 都重新加入到一個(gè)新的 p 的 timer 上

2.4 Reset 時(shí) timer 是如何被操作的?

Reset 的目的是把 timer 重新加入到 timer 堆中,重新等待被觸發(fā)。不過分為兩種情況:

  • 被標(biāo)記為 timerRemoved 的 timer,這種 timer 是已經(jīng)從 timer 堆上刪除了,但會(huì)重新設(shè)置被觸發(fā)時(shí)間,加入到 timer 堆中
  • 等待被觸發(fā)的 timer,在 Reset 函數(shù)中只會(huì)修改其觸發(fā)時(shí)間和狀態(tài)(timerModifiedEarlier或timerModifiedLater)。這個(gè)被修改狀態(tài)的 timer 也同樣會(huì)被重新加入到 timer堆上,不過是由 GMP 觸發(fā)的,由 checkTimers 調(diào)用 adjusttimers 或者 runtimer 來執(zhí)行的。

2.5 Stop 時(shí) timer 是如何被操作的?

time.Stop 為了讓 timer 停止,不再被觸發(fā),也就是從 timer 堆上刪除。不過 timer.Stop 并不會(huì)真正的從 p 的 timer 堆上刪除 timer,只會(huì)將 timer 的狀態(tài)修改為 timerDeleted。然后等待 GMP 觸發(fā)的 adjusttimers 或者 runtimer 來執(zhí)行。

真正刪除 timer 的函數(shù)有兩個(gè) dodeltimer,dodeltimer0。

2.6 Timer 是如何被真正執(zhí)行的?

timer 的真正執(zhí)行者是 GMP。GMP 會(huì)在每個(gè)調(diào)度周期內(nèi),通過 runtime.checkTimers 調(diào)用 timer.runtimer(). timer.runtimer 會(huì)檢查該 p 的 timer 堆上的所有 timer,判斷這些 timer 是否能被觸發(fā)。

如果該 timer 能夠被觸發(fā),會(huì)通過回調(diào)函數(shù) sendTime 給 Timer 的 channel C 發(fā)一個(gè)當(dāng)前時(shí)間,告訴我們這個(gè) timer 已經(jīng)被觸發(fā)了。

如果是 ticker 的話,被觸發(fā)后,會(huì)計(jì)算下一次要觸發(fā)的時(shí)間,重新將 timer 加入 timer 堆中。

3. Timer 使用中的坑

確實(shí) timer 是我們開發(fā)中比較常用的工具,但是 timer 也是最容易導(dǎo)致內(nèi)存泄露,CPU 狂飆的殺手之一。

不過仔細(xì)分析可以發(fā)現(xiàn),其實(shí)能夠造成問題就兩個(gè)方面:

  • 錯(cuò)誤創(chuàng)建很多的 timer,導(dǎo)致資源浪費(fèi)
  • 由于 Stop 時(shí)不會(huì)主動(dòng)關(guān)閉 C,導(dǎo)致程序阻塞

3.1 錯(cuò)誤創(chuàng)建很多 timer,導(dǎo)致資源浪費(fèi)

func main() {
    for {
        // xxx 一些操作
        timeout := time.After(30 * time.Second)
        select {
        case <- someDone:
            // do something
        case <-timeout:
            return
        }
    }
}

上面這段代碼是造成 timer 異常的最常見的寫法,也是我們最容易忽略的寫法。

造成問題的原因其實(shí)也很簡單,因?yàn)?timer.After 底層是調(diào)用的 timer.NewTimer,NewTimer 生成 timer 后,會(huì)將 timer 放入到全局的 timer 堆中。

for 會(huì)創(chuàng)建出來數(shù)以萬計(jì)的 timer 放入到 timer 堆中,導(dǎo)致機(jī)器內(nèi)存暴漲,同時(shí)不管 GMP 周期 checkTimers,還是插入新的 timer 都會(huì)瘋狂遍歷 timer 堆,導(dǎo)致 CPU 異常。

要注意的是,不只 time.After 會(huì)生成 timer, NewTimer,time.AfterFunc 同樣也會(huì)生成 timer 加入到 timer 中,也都要防止循環(huán)調(diào)用。

解決辦法: 使用 time.Reset 重置 timer,重復(fù)利用 timer。

我們已經(jīng)知道 time.Reset 會(huì)重新設(shè)置 timer 的觸發(fā)時(shí)間,然后將 timer 重新加入到 timer 堆中,等待被觸發(fā)調(diào)用。

func main() {
    timer := time.NewTimer(time.Second * 5)    
    for {
        t.Reset(time.Second * 5)

        select {
        case <- someDone:
            // do something
        case <-timer.C:
            return
        }
    }
}

3.2 程序阻塞,造成內(nèi)存或者 goroutine 泄露

func main() {
    timer1 := time.NewTimer(2 * time.Second)
    <-timer1.C
    println("done")
}

上面的代碼可以看出來,只有等待 timer 超時(shí) "done" 才會(huì)輸出,原理很簡單:程序阻塞在 <-timer1.C 上,一直等待 timer 被觸發(fā)時(shí),回調(diào)函數(shù) time.sendTime 才會(huì)發(fā)送一個(gè)當(dāng)前時(shí)間到 timer1.C 上,程序才能繼續(xù)往下執(zhí)行。

不過使用 timer.Stop 的時(shí)候就要特別注意了,比如:

func main() {
    timer1 := time.NewTimer(2 * time.Second)
    go func() {
        timer1.Stop()
    }()
    <-timer1.C

    println("done")
}

程序就會(huì)一直死鎖了,因?yàn)?timer1.Stop 并不會(huì)關(guān)閉 channel C,使程序一直阻塞在 timer1.C 上。

上面這個(gè)例子過于簡單了,試想下如果 <- timer1.C 是阻塞在子協(xié)程中,timer 被的 Stop 方法被調(diào)用,那么子協(xié)程可能就會(huì)被永遠(yuǎn)的阻塞在那里,造成 goroutine 泄露,內(nèi)存泄露。

Stop 的正確的使用方式:

func main() {
    timer1 := time.NewTimer(2 * time.Second)
    go func() {
        if !timer1.Stop() {
            <-timer1.C
        }
    }()

    select {
    case <-timer1.C:
        fmt.Println("expired")
    default:
    }
    println("done")
}

到此這篇關(guān)于Go timer如何調(diào)度 的文章就介紹到這了,更多相關(guān)Go timer 調(diào)度 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • go?mod文件內(nèi)容版本號(hào)簡單用法詳解

    go?mod文件內(nèi)容版本號(hào)簡單用法詳解

    這篇文章主要為大家介紹了go?mod文件內(nèi)容版本號(hào)簡單用法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-10-10
  • Go語言中常用的基礎(chǔ)方法總結(jié)

    Go語言中常用的基礎(chǔ)方法總結(jié)

    這篇文章主要為大家詳細(xì)介紹了Go語言中常用的一些基礎(chǔ)方法,例如:使用正則表達(dá)式驗(yàn)證字符串、格式化字符串、時(shí)間的比較等等,需要的可以參考一下
    2022-09-09
  • Golang設(shè)計(jì)模式之原型模式詳細(xì)講解

    Golang設(shè)計(jì)模式之原型模式詳細(xì)講解

    如果一個(gè)類的有非常多的屬性,層級(jí)還很深。每次構(gòu)造起來,不管是直接構(gòu)造還是用建造者模式,都要對(duì)太多屬性進(jìn)行復(fù)制,那么有沒有一種好的方式讓我們創(chuàng)建太的時(shí)候使用體驗(yàn)更好一點(diǎn)呢? 今天的文章里就給大家介紹一種設(shè)計(jì)模式,來解決這個(gè)問題
    2023-01-01
  • 一文帶你了解Go語言如何解析JSON

    一文帶你了解Go語言如何解析JSON

    本文將說明如何利用?Go?語言將?JSON?解析為結(jié)構(gòu)體和數(shù)組,如果解析?JSON?的嵌入對(duì)象,如何將?JSON?的自定義屬性名稱映射到結(jié)構(gòu)體,如何解析非結(jié)構(gòu)化的?JSON?字符串
    2023-01-01
  • Go語言里的new函數(shù)用法分析

    Go語言里的new函數(shù)用法分析

    這篇文章主要介紹了Go語言里的new函數(shù)用法,實(shí)例分析了new函數(shù)的功能及使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02
  • Go設(shè)計(jì)模式之備忘錄模式講解和代碼示例

    Go設(shè)計(jì)模式之備忘錄模式講解和代碼示例

    備忘錄是一種行為設(shè)計(jì)模式, 允許生成對(duì)象狀態(tài)的快照并在以后將其還原,本文就通過代碼示例給大家講講Go備忘錄模式,感興趣的小伙伴跟著小編一起來看看吧
    2023-08-08
  • Go語言繼承功能使用結(jié)構(gòu)體實(shí)現(xiàn)代碼重用

    Go語言繼承功能使用結(jié)構(gòu)體實(shí)現(xiàn)代碼重用

    今天我來給大家介紹一下在?Go?語言中如何實(shí)現(xiàn)類似于繼承的功能,讓我們的代碼更加簡潔和可重用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • Go語言在Linux環(huán)境下輸出彩色字符的方法

    Go語言在Linux環(huán)境下輸出彩色字符的方法

    這篇文章主要介紹了Go語言在Linux環(huán)境下輸出彩色字符的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-04-04
  • 基于context.Context的Golang?loader緩存請(qǐng)求放大問題解決

    基于context.Context的Golang?loader緩存請(qǐng)求放大問題解決

    這篇文章主要為大家介紹了基于context.Context的Golang?loader緩存請(qǐng)求放大解決方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • go等待一組協(xié)程結(jié)束的操作方式

    go等待一組協(xié)程結(jié)束的操作方式

    這篇文章主要介紹了go等待一組協(xié)程結(jié)束的操作方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05

最新評(píng)論

克拉玛依市| 青海省| 鸡东县| 井研县| 城步| 天台县| 亳州市| 镇江市| 达日县| 介休市| 阳山县| 贵德县| 泾源县| 中阳县| 大荔县| 郓城县| 迭部县| 湟中县| 康马县| 霍林郭勒市| 武安市| 新丰县| 合水县| 时尚| 保定市| 东乌| 彭泽县| 稻城县| 科技| 贞丰县| 伊春市| 乌鲁木齐市| 林甸县| 张掖市| 手游| 阿拉善右旗| 海宁市| 乳山市| 万年县| 遂川县| 芜湖县|