遊戲服務端定時器的實現

2022-08-01 16:39:16 字數 3001 閱讀 6900

最近在看過一些定時器相關的資料,也讀了一些**,比如雲風的skynet的定時器實現,小有啟發,因此將所得整理記錄下來。  

通常,乙個定時器模組會提供以下三個介面:

reg_tick(timeout, callback);

unreg_tick(tick_id);

update_timer();

reg_tick註冊乙個tick,unreg_tick取消乙個tick,update_timer更新計時器的時間,觸發其中過期的tick。前兩個介面好理解,問題是第三個介面update_timer,到底什麼時候呼叫這個呢?看下面的說明。

乙個遊戲服務端需要處理客戶端的請求,也要處理定時的任務,其主迴圈或許如下:

1

while 1:2#

處理網路io任務

3 result = select(10)

4for fd, event in

result:

5handle(fd, event)6#

處理定時任務

7 update_timer()

update_time這個函式的工作就是找出伺服器所有過期的定時任務,並執行其對應的**函式。最簡單的實現是,假設伺服器有乙個集合儲存著所有註冊了的tick(乙個tick就是乙個定時任務,下文將不再解釋),每次更新計時器的時候,遍歷集合中的所有tick,挨個去判斷這個tick是否過期,如果是則執行其callback,可能的話還要刪掉這個已經觸發了的tick。

1

defupdate_timer():

2 current =get_cur_time()

3for timer in

reg_timer_list:

4if timer.expeires >current:

5 timer.callback()

接下來分析這個實現的時間複雜度。如果使用vector來儲存tick的話,reg_tick的時間是o(1),unreg_tick的時間是o(n),update_timer的時間也是o(n)。

當然有幾種簡單的改進方法,比如改為用hash_map來儲存tick,可以將插入和刪除的時間降到o(1),但需要注意的是對於一般的hash_map如果沒有記錄前乙個和後乙個元素的位置,遍歷起來是會相對耗時的。因此update_timer的時間可能會需要更多,具體依賴hash_map的實現。當然也可以使用紅黑樹來儲存tick。

另一種改進方法是,依然使用佔記憶體相對較小的vector儲存tick,不過需要維持這個vector,使其中所有tick都是按觸發時間從早到晚的依次排序。這樣一來插入和刪除的時間變為o(n),但是相對的,update_timer卻變快了,因為可以把要觸發的tick都集中到一起,攤還下來從而每乙個tick的觸發時間變少了。這樣做通常是有好處的,如上文所述,相對於reg_tick和unreg_tick伺服器一般會較為頻繁的呼叫update_timer這個函式。

其實基於維持乙個有序陣列的思想還可以進一步的優化,比如用觸發時間為key使用最小堆來儲存tick。這種情況插入刪除和有序陣列時間複雜度一致,但是update_timer一般來說效率更高。

其實還可以進一步再優化,這次依然選擇hash_map作為儲存tick的資料結構,只不過儲存在hash_map中的key是乙個時間戳,value是這個時間戳對應的所有tick列表。這樣一來每次呼叫update_timer這個函式不再需要遍歷整個hash_map,而是根據時間戳索引到對應的tick列表,因此update_timer的時間複雜度降至o(1)。由此而來,reg_timer、unreg_timer、update_timer三個介面的時間複雜度都降至o(1)。

1

defupdate_timer():

2 last =get_last_update()

3 current =get_cur_time()

4while last <=current:

5for timer in

reg_timer_dict[last]:

6timer.callback()

7 last += 1

到這為止了嗎?

通用性的解決方案是為了滿足大多數的情況而被使用的,在特定問題下,如果我們根據問題的獨特性進行相應的優化,通常能做的更好,這也是造輪子的意義所在。回到計時器這個問題,我們是否能優化一下hash_map空間複雜度呢?

接下來要引入的就是特定情況下的解決方案。我們可以基於乙個假設進行優化,假設我們不需要註冊乙個很久以後的tick。由此而引出的是分層時間輪計時器,這個也是linux核心使用的定時器演算法(skynet的定時器也是,可以讀一下skynet的**,只有兩百多行)。

我在這裡就簡單說明一下。假設這個有三個時鐘,分別是時分秒。以秒級時鐘說明,這個時鐘有60個槽,代表了一分鐘的60秒,每乙個槽對應存放的是tick列表。我們有乙個變數記錄下當前的秒針的位置,每當秒針走一步,則觸發對應槽的tick列表中的所有tick。假設我要註冊乙個10秒鐘之後觸發的tick,則把這個tick插入到(當前秒針+10)mod 60的位置上即可。這樣60秒內的tick沒什麼問題,但是大於60秒則如何處理?這時就要使用到分鐘級的時鐘了。比如說我需要註冊乙個70秒後觸發的tick,通過計算可以發現這個tick是下一分鐘觸發的,因此把這個tick存放在(當前分針+1)mod 60的位置上。當秒針轉完一輪後,分針需要走一步,這個時候就把當前分針所指向的槽的所有tick都插入到秒級時鐘去。比如說剛剛的70秒後的tick,因為秒針走了一輪,這時候觸發時間變成10秒之後,因此把它插入到(當前秒針+10)mod 60的位置。時針的處理與這個類似。通過這種方法,表示一天需要的空間複雜度是60+60+24=144個槽。

linux核心和skynet的定時器都是使用這個方法,但是它們不是按時分秒這樣分層,而是將32bit按8/6/6/6/6/分成5個部分,也就是有5個時鐘(這裡每乙個時鐘被稱為time vector簡稱tv),原來的秒級時鐘變為2^8=256個槽。這種分層方法的空間複雜度變為256+64+64+64+64=512個槽,支援註冊最長時間的tick是256*64*64*64*64=2^32秒。

C 服務端技術 定時器

這個設計每呼叫一次就會重新註冊,歡迎交流 include include include include include include using namespace std struct tagtime map timelist 利用map如果key是整形,會自動從小到大排序,我們可以把定時器先...

遊戲服務端開發 一

資料儲存伺服器 遊戲中的資料大致分為靜態配置資料和動態的玩家資料。這裡主要討論玩家資料儲存的解決方案。雖然遊戲應用的寫操作要多於讀操作,但是加入快取層仍然有其必要性。多個應用伺服器啟動時從資料庫讀取資料會在瞬間給資料庫造成巨大壓力,如果將相對靜態的資料以檔案的形式放在應用伺服器本地,可以避免這個問題...

遊戲服務端開發 二

應用伺服器的設計 上 應用伺服器的工作有 0 同步廣播玩家的行為 1作為第三方對玩家個體和玩家之間互動行為計算,並將計算結果推送到資料儲存系統 2驅動遊戲中的 npc 3作為乙個特殊的遊戲參與者,與玩家相互作用。應用伺服器最重要的工作莫過於同步廣播玩家之間的行為,使玩家之間能夠互視,多人同時遊戲才有...