時間輪(Timing Wheel)算法-高性能定時器策略筆記

原文轉自:http://www.tanjp.com/archives/199 (即時修正和更新)

 

什麼時候需要定時器?

我們都知道程序是能快速運算出結果,幾乎在一瞬間就可以把結果算出來。但是這個前提是所有輸入條件都拿到手的情況下,如果有些輸入條件 A 你並不知道什麼時候能符合,那怎麼辦?寫一個whlie循環一直檢查?這樣無疑很浪費CPU,顯然行不通。

有經驗程序員可以已經想到辦法,把這些等待輸入條件 A, B, C, …等等全部記錄起來,在其他相關事件觸發時,順便檢查一下這些輸入條件是否滿足?如果滿足就執行某個函數,否則下次再檢查一次。這種做法,一般的業務系統都是可實現的。但是會導致代碼繁瑣而且也不好維護,並且當檢查條件多了會導致系統性能大幅下降。這時候,就需要使用定時器來定時檢查。

還有一種情況是,假設某個遊戲戰鬥邏輯,一個法術攻擊使得某個區域內中毒,持續時間從[t1, t2]。也就是說,在未來確定的時間點會發生某些事情的時候,就要在未來的某個時間點添加定時器事件。

圖文代碼請參照以上筆記

 

時間輪(Timing Wheel)算法

時間輪由多個時間格組成,每個時間格代表當前時間輪的基本時間跨度(tickMs)。時間輪的時間格個數是固定的,可用wheelSize來表示,那麼整個時間輪的總體時間跨度(interval)可以通過公式 tickMs × wheelSize計算得出。時間輪還有一個錶盤指針(currentTime),用來表示時間輪當前所處的時間,currentTime是tickMs的整數倍。currentTime可以將整個時間輪劃分爲到期部分和未到期部分,currentTime當前指向的時間格也屬於到期部分,表示剛好到期,需要處理此時間格所對應的鏈表的所有任務。

若時間輪的tickMs=1ms,wheelSize=20,那麼可以計算得出interval爲20ms。初始情況下表盤指針currentTime指向時間格0,此時有一個定時爲2ms的任務插入進來會存放到時間格爲2的鏈表中。隨着時間的不斷推移,指針currentTime不斷向前推進,過了2ms之後,當到達時間格2時,就需要將時間格2所對應的鏈表中的任務做相應的到期操作。此時若又有一個定時爲8ms的任務插入進來,則會存放到時間格10中,currentTime再過8ms後會指向時間格10。如果同時有一個定時爲19ms的任務插入進來怎麼辦?如果此時有個定時爲350ms的任務該如何處理?當任務的到期時間超過了當前時間輪所表示的時間範圍時,就會嘗試添加到上層時間輪中。

圖文代碼請參照以上筆記

參考上圖,複用之前的案例,第一層的時間輪tickMs=1ms, wheelSize=20, interval=20ms。第二層的時間輪的tickMs爲第一層時間輪的interval,即爲20ms。每一層時間輪的wheelSize是固定的,都是20,那麼第二層的時間輪的總體時間跨度interval爲400ms。以此類推,這個400ms也是第三層的tickMs的大小,第三層的時間輪的總體時間跨度爲8000ms。

 

對於之前所說的350ms的定時任務,顯然第一層時間輪不能滿足條件,所以就升級到第二層時間輪中,最終被插入到第二層時間輪中時間格17所對應的鏈表中。如果此時又有一個定時爲450ms的任務,那麼顯然第二層時間輪也無法滿足條件,所以又升級到第三層時間輪中,最終被插入到第三層時間輪中時間格1的鏈表中。注意到在到期時間在[400ms,800ms)區間的多個任務(比如446ms、455ms以及473ms的定時任務)都會被放入到第三層時間輪的時間格1中,時間格1對應的鏈表的超時時間爲400ms。隨着時間的流逝,當次鏈表到期之時,原本定時爲450ms的任務還剩下50ms的時間,還不能執行這個任務的到期操作。這裏就有一個時間輪降級的操作,會將這個剩餘時間爲50ms的定時任務重新提交到層級時間輪中,此時第一層時間輪的總體時間跨度不夠,而第二層足夠,所以該任務被放到第二層時間輪到期時間爲[40ms,60ms)的時間格中。再經歷了40ms之後,此時這個任務又被“察覺”到,不過還剩餘10ms,還是不能立即執行到期操作。所以還要再有一次時間輪的降級,此任務被添加到第一層時間輪到期時間爲[10ms,11ms)的時間格中,之後再經歷10ms後,此任務真正到期,最終執行相應的到期操作。

 

代碼實現C++

1、基礎節點類型和鏈表元素的添加和刪除。圖文代碼請參照以上筆記

 

2、時間輪,一個輪子。圖文代碼請參照以上筆記

 

3、輪組,多個輪子組合起來。圖文代碼請參照以上筆記

 

4、封裝接口,方便易用。圖文代碼請參照以上筆記

 

應用例子

添加一個Id爲10的1000毫秒的定時器,觸發5次後結束。還有添加一個Id爲20的3000毫秒的定時器,無限觸發。

圖文代碼請參照以上筆記

 

輸出結果:

begin

now=1558125768212—–

now=1558125768220, times=8, had=0 —–

now=1558125769225, times=1005, had=1 —–

expired tid=10

—————-

now=1558125770237, times=1012, had=1 —–

expired tid=10

—————-

now=1558125771245, times=1008, had=1 —–

expired tid=20

expired tid=10

—————-

now=1558125772252, times=1007, had=1 —–

expired tid=10

—————-

now=1558125773256, times=1004, had=1 —–

expired tid=10

closed tid=10

—————-

now=1558125774261, times=1005, had=1 —–

expired tid=20

—————-

now=1558125775269, times=1008, had=0 —–

now=1558125776271, times=1002, had=0 —–

now=1558125777283, times=1012, had=1 —–

expired tid=20

—————-

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章