中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Go定時器內部的實現原理是什么

發布時間:2021-08-06 10:51:58 來源:億速云 閱讀:160 作者:chen 欄目:數據庫

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

前言

本節,我們重點關注系統協程是如何管理這些定器的,包括以下問題:

定時器使用什么數據結構存儲?定時器如何觸發事件?定時器如何添加進系統協程?定時器如何從系統協程中刪除?

定時器存儲

timer數據結構

Timer和Ticker數據結構除名字外完全一樣,二者都含有一個runtimeTimer類型的成員,這個就是系統協程所維護的對象。runtimeTimer類型是time包的名稱,在runtime包中,這個類型叫做timer。

timer數據結構如下所示:

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

其中timersBucket便是系統協程存儲timer的容器,里面有個切片來存儲timer,而i便是timer所在切片的下標。

timersBucket數據結構

我們來看一下timersBucket數據結構:

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

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

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

看到這里應該能明白,系統協程在首次創建定時器時創建,定時器存儲在切片中,系統協程負責計時并維護這個切片。

存儲拓撲

以Ticker為例,我們回顧一下Ticker、timer和timersBucket關系,假設我們已經創建了3個Ticker,那么它們之間的關系如下:

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

timersBucket數組

通過timersBucket數據結構可以看到,系統協程負責計時并維護其中的多個timer,一個timersBucket包含一個系統協程。

當系統中定時器非常多時,一個系統協程可能處理能力跟不上,所以Go在實現時實際上提供了多個timersBucket,也就有多個系統協程來處理定時器。

最理想的情況,應該預留GOMAXPROCS個timersBucket,以便充分使用CPU資源,但需要跟據實際環境動態分配。為了實現簡單,Go在實現時預留了64個timersBucket,絕大部分場景下這些足夠了。

每當協程創建定時器時,使用協程所屬的ProcessID%64來計算定時器存入的timersBucket。

下圖三個協程創建定時器時,定時器分布如下圖所示:

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

一般情況下,同一個Process的協程創建的定時器分布于同一個timersBucket中,只有當GOMAXPROCS大于64時才會出現多個Process分布于同一個timersBucket中。

定時器運行機制

看完上面的數據結構,了解了timer是如何存儲的。現在開始探究定時器內部運作機制。

創建定時器

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

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

創建管道的部分前面已做過介紹,這里我們重點關注timer的啟動部分。

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

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

至此,第一步,給當前的timer選擇一個timersBucket已經完成。

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

源碼src/runtime/time.go:addtimerLocked()函數負責添加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) { // 堆中插入數據,觸發堆重新排序 return false } if t.i == 0 { // 堆排序后,發現新插入的定時器跑到了棧頂,需要喚醒協程來處理 // siftup moved to top: new earliest deadline. if tb.sleeping { // 協程在睡眠,喚醒協程來處理新加入的定時器 tb.sleeping = false notewakeup(&tb.waitnote)} if tb.rescheduling { // 協程已暫停,喚醒協程來處理新加入的定時器 tb.rescheduling = false goready(tb.gp, 0)}} if !tb.created { // 如果是系統首個定時器,則啟動協程處理堆中的定時器 tb.created = true go timerproc(tb)}return true }

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

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

下圖展示一個小頂堆結構,圖中每個圓圈代表一個timer,圓圈中的數字代表距離觸發事件的秒數,圓圈外的數字代表其在切片中的下標。其中timer 15是新加入的,加入后它被最終調整到數組的1號下標。

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

刪除定時器

當Timer執行結束或Ticker調用Stop()時會觸發定時器的刪除。從timersBucket中刪除定時器是添加定時器的逆過程,即堆中元素刪除后,觸發堆調整。在此不再細述。

timerproc

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

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

如果堆中已沒有事件需要觸發,則系統協程將進入暫停態,也可認為是無限時睡眠,直到有新的timer加入才會被喚醒。

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

資源泄露問題

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

首先,創建Ticker的協程并不負責計時,只負責從Ticker的管道中獲取事件; 其次,系統協程只負責定時器計時,向管道中發送事件,并不關心上層協程如何處理事件;

如果創建了Ticker,則系統協程將持續監控該Ticker的timer,定期觸發事件。如果Ticker不再使用且沒有Stop(),那么系統協程負擔會越來越重,最終將消耗大量的CPU資源。

感謝各位的閱讀,以上就是“Go定時器內部的實現原理是什么”的內容了,經過本文的學習后,相信大家對Go定時器內部的實現原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

go
AI

呼和浩特市| 大英县| 都江堰市| 漯河市| 白城市| 互助| 云安县| 清水县| 灵川县| 辽源市| 平武县| 富锦市| 繁峙县| 甘洛县| 潢川县| 玉门市| 陵川县| 尤溪县| 宁南县| 加查县| 佛学| 金平| 左云县| 天镇县| 石林| 剑河县| 育儿| 定州市| 康马县| 武定县| 新乡县| 南召县| 宣城市| 洪洞县| 榆林市| 龙山县| 苏州市| 抚远县| 江安县| 鹤岗市| 安乡县|