簡介
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的時候處理。