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

本節(jié),我們重點關(guān)注系統(tǒng)協(xié)程是如何管理這些定器的,包括以下問題:
定時器使用什么數(shù)據(jù)結(jié)構(gòu)存儲?定時器如何觸發(fā)事件?定時器如何添加進系統(tǒng)協(xié)程?定時器如何從系統(tǒng)協(xié)程中刪除?
Timer和Ticker數(shù)據(jù)結(jié)構(gòu)除名字外完全一樣,二者都含有一個runtimeTimer類型的成員,這個就是系統(tǒng)協(xié)程所維護的對象。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):
type timersBucket struct { lock mutexgp *g // 處理堆中事件的協(xié)程 created bool // 事件處理協(xié)程是否已創(chuàng)建,默認為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é)程負責(zé)計時并維護這個切片。
以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ù)據(jù)結(jié)構(gòu)可以看到,系統(tǒng)協(xié)程負責(zé)計時并維護其中的多個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中。
看完上面的數(shù)據(jù)結(jié)構(gòu),了解了timer是如何存儲的。現(xiàn)在開始探究定時器內(nèi)部運作機制。
回顧一下定時器創(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ù)負責(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的時間是負值,那么會被修改為很大的值,來保證后續(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)整。在此不再細述。
timerproc為系統(tǒng)協(xié)程的具體實現(xiàn)。它是在首次創(chuàng)建定時器創(chuàng)建并啟動的,一旦啟動永不銷毀。 如果timersBucket中有定時器,取出堆頂定時器,計算睡眠時間,然后進入睡眠,醒來后觸發(fā)事件。
某個timer的事件觸發(fā)后,跟據(jù)其是否是周期性定時器來決定將其刪除還是修改時間后重新加入堆。
如果堆中已沒有事件需要觸發(fā),則系統(tǒng)協(xié)程將進入暫停態(tài),也可認為是無限時睡眠,直到有新的timer加入才會被喚醒。
timerproc處理事件的流程圖如下:
前面介紹Ticker時格外提醒不使用的Ticker需要顯式的Stop(),否則會產(chǎn)生資源泄露。研究過timer實現(xiàn)機制后,可以很好的解釋這個問題了。
首先,創(chuàng)建Ticker的協(xié)程并不負責(zé)計時,只負責(zé)從Ticker的管道中獲取事件; 其次,系統(tǒng)協(xié)程只負責(zé)定時器計時,向管道中發(fā)送事件,并不關(guān)心上層協(xié)程如何處理事件;
如果創(chuàng)建了Ticker,則系統(tǒng)協(xié)程將持續(xù)監(jiān)控該Ticker的timer,定期觸發(fā)事件。如果Ticker不再使用且沒有Stop(),那么系統(tǒng)協(xié)程負擔(dān)會越來越重,最終將消耗大量的CPU資源。
感謝各位的閱讀,以上就是“Go定時器內(nèi)部的實現(xiàn)原理是什么”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Go定時器內(nèi)部的實現(xiàn)原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián)網(wǎng)站建設(shè)公司,,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!
本文題目:Go定時器內(nèi)部的實現(xiàn)原理是什么-創(chuàng)新互聯(lián)
URL標(biāo)題:http://www.chinadenli.net/article20/deeoco.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站收錄、靜態(tài)網(wǎng)站、面包屑導(dǎo)航、響應(yīng)式網(wǎng)站、全網(wǎng)營銷推廣、網(wǎng)站維護
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容