skynet源碼解析:計時器

簡介

skynet中提供了一個簡單的計時器實現,可以設置一個超時時間,時間到達後給對應的服務發送消息,這篇文字主要是講講該計時器的實現。

skynet中沒有使用signal信號的方式來實現計時器,而是程序自身進行計時並處理計時器事件。另外,skynet中將時間由近及遠劃分爲五個level,在時間複雜度和空間複雜度上達到了平衡。

API

下面列舉一下skynet_timer.h 中提供的public API:

  • void skynet_timer_init(void);
    • 初始化計時器
  • int skynet_timeout(uint32_t handle, int time, int session);
    • 設置超時時間及相應的回調消息
    • 如果time<=0 , 立馬派送消息
    • 如果time>0, 將其加入計時器列表中,等時間到達後派發。
    • time時間精度爲百分之一秒
  • void skynet_updatetime(void);
    • 程序更新時間並觸發相應計時器事件

數據結構

struct timer_event {         //每個計時器事件回調              
    uint32_t handle;                          
    int session;                              
};                                            

struct timer_node {  //每次分配timer_node 與 timer_event (參數)的空間, 將time_event 放在node 後面                         
    struct timer_node *next;                  
    uint32_t expire;  //計時器事件觸發時間                        
};                                            

struct link_list { //計時器node列表                           
    struct timer_node head;                   
    struct timer_node *tail;                  
};                                            

struct timer {                                
    struct link_list near[TIME_NEAR]; //最近的時間,         
    struct link_list t[4][TIME_LEVEL]; //根據時間久遠分級
    struct spinlock lock;                     
    uint32_t time;        // 計時器,每百分之一秒更新一次             
    uint32_t starttime;   //起始時間 秒                    
    uint64_t current;     // 當前時間與starttime的時間差 單位爲百分之一秒                   
    uint64_t current_point;   //上一次update的時間, 百分之一秒                
};      

值得注意的有以下幾點:

  • timer_node中沒有直接包含timer_event 或者 timer_event pointer, 而是在分配timer_node 時額外分配了參數長度的空間,我想這麼做的好處一是可以支持變長參數,二是可以是的node和event在內存上是連續的。
  • 數據結構中將時間劃分爲五個區域存儲,分佈是near[TIME_NEAR] 和 t[4][TIME_LEVEL], 其中TIMER_NEAR爲(1<<8 -1), TIMER_LEVEL爲(1<<6 -1)。
    • 其原理是先比較當前時間的高24位與期望時間的高24位,如果他們相等,說明期望時間與當前時間接近,那麼就將其存儲到near[index]中,其中index爲期望時間的低八位數字。
    • 如果不滿足上一個要求,就再比較當前時間與期望時間的高18位,如果它們相等,那麼就將其存儲到t[0][index]中,其中index爲期望時間的低九至十五位數字。
    • 依次比較高12位,高6位,將計時器事件存儲到t[1], t[2], t[3]中。

思想

我以前也想過自己實現計時器,我當時的想法是使用一個有序的列表,將每個計時器事件又近及遠添加到列表中,每次更新時間時就可以很方便的獲取需要觸發的事件。但是這樣,每次插入計時器事件的複雜度爲O(n)。

skynet將時間由近及遠分爲5組,每個組裏面再使用hash的思想,是的插入時間複雜度爲O(1), 且所需空間不大。需要注意的是,除了直接加入到near列表中的事件,其他level的事件到了相應時間節點後需要重新分組添加,但是遊戲編程裏的計時器事件絕大多數應該都是在較近時間內的,跨度以月、年爲單位的計時器很少。

實現

這裏主要講的是關於timer.time溢出時的處理,timer.time存儲的是計時器初始化之後計時器觸發的次數(沒百分之一秒更新一次),所以實際上也是計時器當前的時間,它是一個無符號32爲整數,如果程序運行時間較長,會出現期望時間(timer.time + time)出現溢出的情況,在添加timer_node 到計時器時並沒有針對這種情況作特殊處理(溢出時,期望時間是小於當前時間的)。

由於skynet_timeout的time參數爲有符號32爲整數,所有當timer.time最高位爲0時,不會發生溢出情況;當timer.time最高位爲1時,有可能會發生溢出情況,並且溢出後期望時間的最高位一定爲0,這保證了timer.time和期望時間的最高位一定不相等,所以該計時器事件會被添加到timer.t[3][index]中。

skynet中更新時間時,如果沒有發生跨level的情況,就直接處理near中當前時間的計時器,如果發生了跨level的情況,就要將相應level的某些計時器移除並加入到near中。這裏you’g有個特殊情況是噹噹前時間爲0時,需要處理之前溢出情況下加入的計時器,也就是timer.t[3][0]中的計時器。timer.t[3][i](i!=0)中的計時器會在之後跨level的時候處理。

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