久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

Go定時器內(nèi)部的實現(xiàn)原理是什么

154次閱讀
沒有評論

共計 4264 個字符,預(yù)計需要花費 11 分鐘才能閱讀完成。

這篇文章主要講解了“Go 定時器內(nèi)部的實現(xiàn)原理是什么”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著丸趣 TV 小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Go 定時器內(nèi)部的實現(xiàn)原理是什么”吧!

前言

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

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

定時器存儲 timer 數(shù)據(jù)結(jié)構(gòu)

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

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

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

其中 timersBucket 便是系統(tǒng)協(xié)程存儲 timer 的容器,里面有個切片來存儲 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,添加首個定時器時置為 true sleeping bool // 事件處理協(xié)程(gp)是否在睡眠 ( 如果 t 中有定時器,還未到觸發(fā)的時間,那么 gp 會投入睡眠) rescheduling bool // 事件處理協(xié)程(gp)是否已暫停(如果 t 中定時器均已刪除,那么 gp 會暫停)sleepUntil   int64 // 事件處理協(xié)程睡眠時間 waitnote     note // 事件處理協(xié)程睡眠事件(據(jù)此喚醒協(xié)程)t            []*timer // 定時器切片}

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

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

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

存儲拓?fù)?

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

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

timersBucket 數(shù)組

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

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

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

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

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

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

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

定時器運行機(jī)制

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

創(chuàng)建定時器

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

創(chuàng)建一個管道創(chuàng)建一個 timer 并啟動(注意此 timer 不是 Timer,而是系統(tǒng)協(xié)程所管理的 timer。)

創(chuàng)建管道的部分前面已做過介紹,這里我們重點關(guān)注 timer 的啟動部分。

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

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

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

其次,每個 timer 都必須要加入到 timersBucket 中。前面我們知道,timersBucket 中切片中保存著 timer 的指針,新加入的 timer 并不是按加入時間順序存儲的,而是把 timer 按照觸發(fā)的時間排序的一個小頭堆。那么 timer 加入 timersBucket 的過程實際上也是堆排序的過程,只不過這個排序是指的是新加元素后的堆調(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) // 先把定時器插入到堆尾 tb.t = append(tb.t, t) // 保存定時器 if !siftupTimer(tb.t, t.i) {// 堆中插入數(shù)據(jù),觸發(fā)堆重新排序 return false} if t.i == 0 {// 堆排序后,發(fā)現(xiàn)新插入的定時器跑到了棧頂,需要喚醒協(xié)程來處理 // siftup moved to top: new earliest deadline. if tb.sleeping { // 協(xié)程在睡眠,喚醒協(xié)程來處理新加入的定時器 tb.sleeping = false notewakeup( tb.waitnote)} if tb.rescheduling {// 協(xié)程已暫停,喚醒協(xié)程來處理新加入的定時器 tb.rescheduling = false goready(tb.gp, 0)}} if !tb.created {// 如果是系統(tǒng)首個定時器,則啟動協(xié)程處理堆中的定時器 tb.created = true go timerproc(tb)}return true }

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

如果 timer 的時間是負(fù)值,那么會被修改為很大的值,來保證后續(xù)定時算法的正確性;系統(tǒng)協(xié)程是在首次添加 timer 時創(chuàng)建的,并不是一直存在;新加入 timer 后,如果新的 timer 跑到了棧頂,意味著新的 timer 需要立即處理,那么會喚醒系統(tǒng)協(xié)程。

下圖展示一個小頂堆結(jié)構(gòu),圖中每個圓圈代表一個 timer,圓圈中的數(shù)字代表距離觸發(fā)事件的秒數(shù),圓圈外的數(shù)字代表其在切片中的下標(biāo)。其中 timer 15 是新加入的,加入后它被最終調(diào)整到數(shù)組的 1 號下標(biāo)。

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

刪除定時器

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

timerproc

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

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

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

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

資源泄露問題

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

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

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

感謝各位的閱讀,以上就是“Go 定時器內(nèi)部的實現(xiàn)原理是什么”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對 Go 定時器內(nèi)部的實現(xiàn)原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是丸趣 TV,丸趣 TV 小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

正文完
 
丸趣
版權(quán)聲明:本站原創(chuàng)文章,由 丸趣 2023-08-03發(fā)表,共計4264字。
轉(zhuǎn)載說明:除特殊說明外本站除技術(shù)相關(guān)以外文章皆由網(wǎng)絡(luò)搜集發(fā)布,轉(zhuǎn)載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 舒兰市| 莱西市| 盐山县| 安顺市| 阿鲁科尔沁旗| 浑源县| 抚远县| 镇巴县| 洪湖市| 海门市| 轮台县| 巴楚县| 江川县| 亳州市| 台北县| 昔阳县| 壤塘县| 石楼县| 龙门县| 渭南市| 东台市| 宿迁市| 治县。| 开江县| 清流县| 大丰市| 无锡市| 新泰市| 马尔康县| 阿拉善左旗| 东城区| 乌恰县| 光山县| 湘阴县| 荥阳市| 太原市| 钦州市| 迁西县| 陆丰市| 阜康市| 金华市|