一、協(xié)程介紹協(xié)程是一種用戶級輕量線程,,不僅擁有自己的寄存器上下文和棧,,而且可以由用戶自主調(diào)度執(zhí)行,我們可以在一個線程里面輕松創(chuàng)建數(shù)十萬個協(xié)程,,就像數(shù)十萬次普通函數(shù)調(diào)用一樣輕松,。相對于普通函數(shù)只有一次進出,協(xié)程可以有多次進出的能力,,它通過將函數(shù)上下文數(shù)據(jù)(主要指寄存器和函數(shù)棧)保存起來,,在特定的時刻恢復回去繼續(xù)執(zhí)行來實現(xiàn)的,。在linux里,可以通過getcontext和swapcontext等接口來實現(xiàn)協(xié)程,。 下面展示了一個典型協(xié)程應用的場景,,某個功能分為3個步驟,分布在兩個進程A,、B上,,A執(zhí)行步驟1,需要向B發(fā)出消息完成步驟2,,等待返回后完成步驟3:
可以看到協(xié)程擁有將一個異步過程轉(zhuǎn)化為同步過程的非凡能力,,大大減輕了我們的開發(fā)工作量。 協(xié)程主要分為對稱式(symmetric),、非對稱(asymmetric)式兩種(參見boost協(xié)程庫),,兩者的主要區(qū)別在于: 1、對稱協(xié)程只提供一種傳遞操作,,用于在協(xié)程間直接傳遞控制,,協(xié)程每次需要掛起時需要指定一個明確切換的目標協(xié)程,也就是說控制權(quán)只能在協(xié)程間跳轉(zhuǎn),。 2,、非對稱協(xié)程提供調(diào)用和掛起兩種操作,掛起時控制權(quán)返回給調(diào)用者,。被調(diào)用的協(xié)程可以看成時從屬于調(diào)用者,,這種協(xié)程在日常使用中更常見,上文的例子就屬于非對稱式,。 二,、協(xié)程在游戲中的應用游戲服務端往往會按照業(yè)務特點拆分成多種角色,例如天涯明月刀服務器主要包含:world(負責玩家登錄,、場景管理等),、scene(場景服務器)、social(幫派),、home(家園),、auction(拍賣)等。玩家從一個場景跳轉(zhuǎn)到另外一個場景,,牽涉到world和scene兩種角色的服務器,,主要包括創(chuàng)建新場景、離開舊場景,、進入新場景,、舊場景的銷毀四個異步操作,狀態(tài)轉(zhuǎn)換如下: 兩個狀態(tài)機定義代碼如下: 我們可以看到場景跳轉(zhuǎn)過程中的每一個異步操作都帶來了一個中間狀態(tài),,比如創(chuàng)建新場景帶來了actor_in_waitlist狀態(tài),、離開舊場景帶來了actor_wait_leave_old狀態(tài),。這么多的中間狀態(tài)不僅帶來了開發(fā)上的難度,而且使項目的維護成本極高,,使用協(xié)程對跳轉(zhuǎn)過程中的中間狀態(tài)進行收斂就顯得格外有必要,。 將這四個異步操作歸并到一個協(xié)程trans中,如下所示: 最終玩家和場景的眾多狀態(tài)得到了有效的收斂,,結(jié)果如下: 三,、天涯明月刀協(xié)程應用存在的問題天涯明月刀服務器通過共享內(nèi)存實現(xiàn)了一種resume的機制,將狀態(tài)數(shù)據(jù)存放在共享內(nèi)存中,,游戲進程crash后再次重啟attach原共享內(nèi)存,,所有狀態(tài)得到恢復,玩家不受影響,。resume機制使游戲容災能力得到很大程度的提升,,但同時也讓上文所述的普通協(xié)程失去了作用,主要有以下兩點原因: (1)resume后?;刂?、堆基地址等發(fā)生了變化,寄存器上下文中保存的地址將會失效,; (2)協(xié)程棧內(nèi)用戶使用的指針也將失效,; 如果我們切換時不保存函數(shù)的上下文(寄存器上下文和棧),僅記錄函數(shù)的執(zhí)行位置,,并作為函數(shù)的一個參數(shù)傳遞,,函數(shù)每次調(diào)用時根據(jù)這個參數(shù)直接跳到相應的位置執(zhí)行,函數(shù)的位置作為一個值記錄在共享內(nèi)存中,,這樣自然的解決了普通協(xié)程失效的問題,?;谶@種思路,,利用switch case實現(xiàn)了一種無棧協(xié)程的方案,成功模擬了普通協(xié)程的語義,,取得了很好的應用效果,。 我們首先來看一下一個叫達夫設備(Duff's Device)的代碼: voidsend_duff(char *to, char *from, int count) { int n = (count 7) / 8; switch(count% 8) { case0: do { *to = *from ; case7: *to = *from ; case6: *to = *from ; case5: *to = *from ; case4: *to = *from ; case3: *to = *from ; case2: *to = *from ; case1: *to = *from ; }while(--n > 0); } } 代碼的結(jié)構(gòu)顯得非常巧妙,把一個switch語句和一個do-while語句糅合在了一起,。程序的執(zhí)行流程是:程序一開始順序執(zhí)行,,當它執(zhí)行到了switch的時候,就會根據(jù)n的值,,直接跳轉(zhuǎn)到 case n那里,,程序繼續(xù)順序執(zhí)行,當它執(zhí)行到while那里時,,就會判斷循環(huán)條件,。若為真,,則while循環(huán)開始,程序跳轉(zhuǎn)到do那里開始執(zhí)行循環(huán),;為假,,則退出循環(huán),即程序中止,。 這段代碼的本意是為了提高執(zhí)行的效率(一次比較能帶來多個賦值),,但是我們在這主要關(guān)注它的奇特語法,可以看到switch case分支可以滲透到代碼中的任意地方,,如果能在我們需要異步處理的地方加上case分支后離開,,異步處理回來后可以利用switch直接跳轉(zhuǎn)到case分支處繼續(xù)往下執(zhí)行,從而模擬了普通協(xié)程的yield語義,。 利用 swich case對普通函數(shù)進行改造,,使其成為一個協(xié)程,改造過程如下:
改造完的代碼比較奇特,,建議將代碼實際運行一遍,,可以跟蹤到在step1后flag設置為1后函數(shù)退出,切換回來后跳過step1后執(zhí)行step2,,達到了我們需要的效果,。 閱讀上面的代碼馬上可以發(fā)現(xiàn)它的一個致命缺點:可讀性太差、不便于理解維護,。是否可以利用宏定義來簡化上面的代碼,,其中(1)定義如下: (2),、(3)定義如下:
經(jīng)簡化后協(xié)程代碼如下:
CORO_YIELD_IMPL中需要傳入1,、2等位置參數(shù),這種參數(shù)對協(xié)程實際使用者沒有任何作用,,反而增加了理解負擔,,可以利用編譯器計數(shù)器__COUNTER__自動設置位置參數(shù):
至此,一個通用的無棧協(xié)程基本實現(xiàn),,我們只要對函數(shù)增加一個位置參數(shù)并默認為0,,即可將其改造為一個協(xié)程,最終一個協(xié)程如下:
只要將coro_data保存起來,,隨時都可以再次恢復現(xiàn)場,,做到了使異步操作同步化,降低了異步開發(fā)的復雜性,、提高了代碼的可維護性,。在天刀的實際應用中根據(jù)應用場景還會對上述無棧協(xié)程進一步封裝,實現(xiàn)了一個協(xié)程管理器,,實現(xiàn)了更多的功能,,這個以后有機會還會談到,。 四、總結(jié) 上文所述的無棧協(xié)程成功的解決了普通有棧協(xié)程在resume失效的問題,,同有棧協(xié)程相比,,優(yōu)點: 1、切換時,,不涉及內(nèi)核態(tài)切換,,類似于普通函數(shù)調(diào)用效率更高。 2,、不用考慮有棧協(xié)程中的由于棧開辟空間太小導致的棧溢出的問題,。 3、代碼小巧,,跨平臺,。 缺點: 由于其不保存上下文,函數(shù)棧內(nèi)的零時變量都不會保存,,只能依賴傳遞的參數(shù)再次恢復數(shù)據(jù),。 |
|