日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Go 定时器内部实现原理剖析

發(fā)布時(shí)間:2025/3/8 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Go 定时器内部实现原理剖析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

為什么80%的碼農(nóng)都做不了架構(gòu)師?>>> ??

前言

前面我們介紹了一次性定時(shí)器Timer和周期性定時(shí)器Ticker,這兩種定時(shí)器內(nèi)部實(shí)現(xiàn)機(jī)制相同。創(chuàng)建定時(shí)器的協(xié)程并不負(fù)責(zé)計(jì)時(shí),而是把任務(wù)交給系統(tǒng)協(xié)程,系統(tǒng)協(xié)程統(tǒng)一處理所有的定時(shí)器。

本節(jié),我們重點(diǎn)關(guān)注系統(tǒng)協(xié)程是如何管理這些定器的,包括以下問題:

  • 定時(shí)器使用什么數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)?
  • 定時(shí)器如何觸發(fā)事件?
  • 定時(shí)器如何添加進(jìn)系統(tǒng)協(xié)程?
  • 定時(shí)器如何從系統(tǒng)協(xié)程中刪除?

定時(shí)器存儲(chǔ)

timer數(shù)據(jù)結(jié)構(gòu)

Timer和Ticker數(shù)據(jù)結(jié)構(gòu)除名字外完全一樣,二者都含有一個(gè)runtimeTimer類型的成員,這個(gè)就是系統(tǒng)協(xié)程所維護(hù)的對(duì)象。 runtimeTimer類型是time包的名稱,在runtime包中,這個(gè)類型叫做timer。

timer數(shù)據(jù)結(jié)構(gòu)如下所示:

type timer struct {tb *timersBucket // the bucket the timer lives in // 當(dāng)前定時(shí)器寄存于系統(tǒng)timer堆的地址i int // heap index // 當(dāng)前定時(shí)器寄存于系統(tǒng)timer堆的下標(biāo)when int64 // 當(dāng)前定時(shí)器下次觸發(fā)時(shí)間period int64 // 當(dāng)前定時(shí)器周期觸發(fā)間隔(如果是Timer,間隔為0,表示不重復(fù)觸發(fā))f func(interface{}, uintptr) // 定時(shí)器觸發(fā)時(shí)執(zhí)行的函數(shù)arg interface{} // 定時(shí)器觸發(fā)時(shí)執(zhí)行函數(shù)傳遞的參數(shù)一seq uintptr // 定時(shí)器觸發(fā)時(shí)執(zhí)行函數(shù)傳遞的參數(shù)二(該參數(shù)只在網(wǎng)絡(luò)收發(fā)場(chǎng)景下使用) }

其中timersBucket便是系統(tǒng)協(xié)程存儲(chǔ)timer的容器,里面有個(gè)切片來存儲(chǔ)timer,而i便是timer所在切片的下標(biāo)。

timersBucket數(shù)據(jù)結(jié)構(gòu)

我們來看一下timersBucket數(shù)據(jù)結(jié)構(gòu):

type timersBucket struct {lock mutexgp *g // 處理堆中事件的協(xié)程created bool // 事件處理協(xié)程是否已創(chuàng)建,默認(rèn)為false,添加首個(gè)定時(shí)器時(shí)置為truesleeping bool // 事件處理協(xié)程(gp)是否在睡眠(如果t中有定時(shí)器,還未到觸發(fā)的時(shí)間,那么gp會(huì)投入睡眠)rescheduling bool // 事件處理協(xié)程(gp)是否已暫停(如果t中定時(shí)器均已刪除,那么gp會(huì)暫停)sleepUntil int64 // 事件處理協(xié)程睡眠時(shí)間waitnote note // 事件處理協(xié)程睡眠事件(據(jù)此喚醒協(xié)程)t []*timer // 定時(shí)器切片 }

“Bucket”譯成中文意為"桶",顧名思義,timersBucket意為存儲(chǔ)timer的容器。

  • lock: 互斥鎖,在timer增加和刪除時(shí)需要使用;
  • gp: 事件處理協(xié)程,就是我們所說的系統(tǒng)協(xié)程,這個(gè)協(xié)程在首次創(chuàng)建Timer或Ticker時(shí)生成;
  • create: 狀態(tài)值,表示系統(tǒng)協(xié)程是否創(chuàng)建;
  • sleeping: 系統(tǒng)協(xié)程是否在睡眠;
  • rescheduling: 系統(tǒng)協(xié)程是否已暫停;
  • sleepUntil: 系統(tǒng)協(xié)程睡眠到指定的時(shí)間(如果有新的定時(shí)任務(wù)可能會(huì)提前喚醒);
  • waitnote: 提前喚醒時(shí)使用的通知;
  • t: 保存timer的切片,當(dāng)調(diào)用NewTimer()或NewTicker()時(shí)便會(huì)有新的timer存到此切片中;

看到這里應(yīng)該能明白,系統(tǒng)協(xié)程在首次創(chuàng)建定時(shí)器時(shí)創(chuàng)建,定時(shí)器存儲(chǔ)在切片中,系統(tǒng)協(xié)程負(fù)責(zé)計(jì)時(shí)并維護(hù)這個(gè)切片。

存儲(chǔ)拓?fù)?/h3>

以Ticker為例,我們回顧一下Ticker、timer和timersBucket關(guān)系,假設(shè)我們已經(jīng)創(chuàng)建了3個(gè)Ticker,那么它們之間的關(guān)系如下:

用戶創(chuàng)建Ticker時(shí)會(huì)生成一個(gè)timer,這個(gè)timer指向timersBucket,timersBucket記錄timer的指針。

timersBucket數(shù)組

通過timersBucket數(shù)據(jù)結(jié)構(gòu)可以看到,系統(tǒng)協(xié)程負(fù)責(zé)計(jì)時(shí)并維護(hù)其中的多個(gè)timer,一個(gè)timersBucket包含一個(gè)系統(tǒng)協(xié)程。

當(dāng)系統(tǒng)中定時(shí)器非常多時(shí),一個(gè)系統(tǒng)協(xié)程可能處理能力跟不上,所以Go在實(shí)現(xiàn)時(shí)實(shí)際上提供了多個(gè)timersBucket,也就有多個(gè)系統(tǒng)協(xié)程來處理定時(shí)器。

最理想的情況,應(yīng)該預(yù)留GOMAXPROCS個(gè)timersBucket,以便充分使用CPU資源,但需要跟據(jù)實(shí)際環(huán)境動(dòng)態(tài)分配。為了實(shí)現(xiàn)簡(jiǎn)單,Go在實(shí)現(xiàn)時(shí)預(yù)留了64個(gè)timersBucket,絕大部分場(chǎng)景下這些足夠了。

每當(dāng)協(xié)程創(chuàng)建定時(shí)器時(shí),使用協(xié)程所屬的ProcessID%64來計(jì)算定時(shí)器存入的timersBucket。

下圖三個(gè)協(xié)程創(chuàng)建定時(shí)器時(shí),定時(shí)器分布如下圖所示:

為描述方便,上圖中3個(gè)協(xié)程均分布于3個(gè)Process中。

一般情況下,同一個(gè)Process的協(xié)程創(chuàng)建的定時(shí)器分布于同一個(gè)timersBucket中,只有當(dāng)GOMAXPROCS大于64時(shí)才會(huì)出現(xiàn)多個(gè)Process分布于同一個(gè)timersBucket中。

定時(shí)器運(yùn)行機(jī)制

看完上面的數(shù)據(jù)結(jié)構(gòu),了解了timer是如何存儲(chǔ)的。現(xiàn)在開始探究定時(shí)器內(nèi)部運(yùn)作機(jī)制。

創(chuàng)建定時(shí)器

回顧一下定時(shí)器創(chuàng)建過程,創(chuàng)建Timer或Ticker實(shí)際上分為兩步:

  • 創(chuàng)建一個(gè)管道
  • 創(chuàng)建一個(gè)timer并啟動(dòng)(注意此timer不是Timer,而是系統(tǒng)協(xié)程所管理的timer。)
  • 創(chuàng)建管道的部分前面已做過介紹,這里我們重點(diǎn)關(guān)注timer的啟動(dòng)部分。

    首先,每個(gè)timer都必須要?dú)w屬于某個(gè)timersBucket的,所以第一步是先選擇一個(gè)timersBucket,選擇的算法很簡(jiǎn)單,將當(dāng)前協(xié)程所屬的Processor ID 與timersBucket數(shù)組長(zhǎng)度求模,結(jié)果就是timersBucket數(shù)組的下標(biāo)。

    const timersLen = 64 var timers [timersLen]struct { // timersBucket數(shù)組,長(zhǎng)度為64timersBucket } func (t *timer) assignBucket() *timersBucket {id := uint8(getg().m.p.ptr().id) % timersLen // Processor ID 與數(shù)組長(zhǎng)度求模,得到下標(biāo)t.tb = &timers[id].timersBucketreturn t.tb }

    至此,第一步,給當(dāng)前的timer選擇一個(gè)timersBucket已經(jīng)完成。

    其次,每個(gè)timer都必須要加入到timersBucket中。前面我們知道,timersBucket中切片中保存著timer的指針,新加入的timer并不是按加入時(shí)間順序存儲(chǔ)的,而是把timer按照觸發(fā)的時(shí)間排序的一個(gè)小頭堆。那么timer加入timersBucket的過程實(shí)際上也是堆排序的過程,只不過這個(gè)排序是指的是新加元素后的堆調(diào)整過程。

    源碼src/runtime/time.go:addtimerLocked()函數(shù)負(fù)責(zé)添加timer:

    func (tb *timersBucket) addtimerLocked(t *timer) bool {if t.when < 0 {t.when = 1<<63 - 1}t.i = len(tb.t) // 先把定時(shí)器插入到堆尾tb.t = append(tb.t, t) // 保存定時(shí)器if !siftupTimer(tb.t, t.i) { // 堆中插入數(shù)據(jù),觸發(fā)堆重新排序return false}if t.i == 0 { // 堆排序后,發(fā)現(xiàn)新插入的定時(shí)器跑到了棧頂,需要喚醒協(xié)程來處理// siftup moved to top: new earliest deadline.if tb.sleeping { // 協(xié)程在睡眠,喚醒協(xié)程來處理新加入的定時(shí)器tb.sleeping = falsenotewakeup(&tb.waitnote)}if tb.rescheduling { // 協(xié)程已暫停,喚醒協(xié)程來處理新加入的定時(shí)器tb.rescheduling = falsegoready(tb.gp, 0)}}if !tb.created { // 如果是系統(tǒng)首個(gè)定時(shí)器,則啟動(dòng)協(xié)程處理堆中的定時(shí)器tb.created = truego timerproc(tb)}return true }

    跟據(jù)注釋來理解上面的代碼比較簡(jiǎn)單,這里附加幾點(diǎn)說明:

  • 如果timer的時(shí)間是負(fù)值,那么會(huì)被修改為很大的值,來保證后續(xù)定時(shí)算法的正確性;
  • 系統(tǒng)協(xié)程是在首次添加timer時(shí)創(chuàng)建的,并不是一直存在;
  • 新加入timer后,如果新的timer跑到了棧頂,意味著新的timer需要立即處理,那么會(huì)喚醒系統(tǒng)協(xié)程。
  • 下圖展示一個(gè)小頂堆結(jié)構(gòu),圖中每個(gè)圓圈代表一個(gè)timer,圓圈中的數(shù)字代表距離觸發(fā)事件的秒數(shù),圓圈外的數(shù)字代表其在切片中的下標(biāo)。其中timer 15是新加入的,加入后它被最終調(diào)整到數(shù)組的1號(hào)下標(biāo)。

    上圖展示的是二叉堆,實(shí)際上Go實(shí)現(xiàn)時(shí)使用的是四叉堆,使用四叉堆的好處是堆的高度降低,堆調(diào)整時(shí)更快。

    刪除定時(shí)器

    當(dāng)Timer執(zhí)行結(jié)束或Ticker調(diào)用Stop()時(shí)會(huì)觸發(fā)定時(shí)器的刪除。從timersBucket中刪除定時(shí)器是添加定時(shí)器的逆過程,即堆中元素刪除后,觸發(fā)堆調(diào)整。在此不再細(xì)述。

    timerproc

    timerproc為系統(tǒng)協(xié)程的具體實(shí)現(xiàn)。它是在首次創(chuàng)建定時(shí)器創(chuàng)建并啟動(dòng)的,一旦啟動(dòng)永不銷毀。 如果timersBucket中有定時(shí)器,取出堆頂定時(shí)器,計(jì)算睡眠時(shí)間,然后進(jìn)入睡眠,醒來后觸發(fā)事件。

    某個(gè)timer的事件觸發(fā)后,跟據(jù)其是否是周期性定時(shí)器來決定將其刪除還是修改時(shí)間后重新加入堆。

    如果堆中已沒有事件需要觸發(fā),則系統(tǒng)協(xié)程將進(jìn)入暫停態(tài),也可認(rèn)為是無限時(shí)睡眠,直到有新的timer加入才會(huì)被喚醒。

    timerproc處理事件的流程圖如下:

    資源泄露問題

    前面介紹Ticker時(shí)格外提醒不使用的Ticker需要顯式的Stop(),否則會(huì)產(chǎn)生資源泄露。研究過timer實(shí)現(xiàn)機(jī)制后,可以很好的解釋這個(gè)問題了。

    首先,創(chuàng)建Ticker的協(xié)程并不負(fù)責(zé)計(jì)時(shí),只負(fù)責(zé)從Ticker的管道中獲取事件; 其次,系統(tǒng)協(xié)程只負(fù)責(zé)定時(shí)器計(jì)時(shí),向管道中發(fā)送事件,并不關(guān)心上層協(xié)程如何處理事件;

    如果創(chuàng)建了Ticker,則系統(tǒng)協(xié)程將持續(xù)監(jiān)控該Ticker的timer,定期觸發(fā)事件。如果Ticker不再使用且沒有Stop(),那么系統(tǒng)協(xié)程負(fù)擔(dān)會(huì)越來越重,最終將消耗大量的CPU資源。

    贈(zèng)人玫瑰手留余香,如果覺得不錯(cuò)請(qǐng)給個(gè)贊~

    本篇文章已歸檔到GitHub項(xiàng)目,求星~ 點(diǎn)我即達(dá)

    轉(zhuǎn)載于:https://my.oschina.net/renhc/blog/3040727

    與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖

    總結(jié)

    以上是生活随笔為你收集整理的Go 定时器内部实现原理剖析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。