首先貼下多進程單線程和單進程多線程的特點:
多進程:有獨立的地址空間,進程之間不共享內存和變量,但可以通過共享內存實現,每個進程只有一個線程,一般用於單機系統開發。
多線程:在同一個進程下的所有線程可以共享內存和變量。
而共同點是,同開辟的進程數/線程數多於系統cpu核數時,無法繼續提高應用的性能。
而多線程架構的服務器,只要適當將一些任務分出來用新的進程啟動,就可以擴展成分布式架構,使用tcp通信即可。當然多進程也可以這么干,通信方式也是使用tcp。
而操作系統對於線程的切換是比進程的切換要快。
下面先介紹下多進程單線程服務器架構,以單機系統為例:
先貼架構圖:
一個游戲服大概就有這幾個進程。
router:作用如其名,路由。 每個功能進程啟動時,會先連接router,router會給連上來的進程分配一個唯一標識,所有功能進程都是靠這個router進程通信。
login: 登錄服務器,client登錄驗證在這個進程進行。
logic:玩家單人邏輯操作處理進程,login會將登錄的玩家平攤到這些logic上。
global_logic: 全局操作進程,多人玩法的功能,例如戰斗匹配,工會等操作會放在這里進行。
log:游戲日志輸出進程,所有功能進程的日志輸出都先發到這個進程,log進程再輸出到磁盤文件,
db: redis作為內存數據庫,mysql作為數據持久化,其他功能進程取數據都會發送請求到db。
back: 后台進程,集成了一個http服務器,可以處理http請求,這里可以集成一些第三方服務功能,如gm指令。
以上每個進程都是單線程,所以無需考慮鎖的問題。
對於每個進程收發數據:
發數據:直接把 {target_id: data} 發送到router,
收數據:幀驅動,如100ms主動向router詢問是否有數據,有則取過來進行處理。
單機系統下,如果采用共享內存方式,通信效率將非常高。
所以多進程的服務器架構設計起來還是比較簡單的。
再介紹下多線程服務器的架構,這里我想介紹actor模型。
一個Actor指的是一個最基本的計算單元。它能接收一個消息並且基於其執行計算。
這個理念很像面向對象語言,一個對象接收一條消息(方法調用),然后根據接收的消息做事(調用了哪個方法)。
Actors一大重要特征在於actors之間相互隔離,它們並不互相共享內存。這點區別於上述的對象。也就是說,一個actor能維持一個私有的狀態,並且這個狀態不可能被另一個
actor所改變。
每個Actor都有一個郵箱,用於接收其他actor發送的消息。
這里重點要講下Actor模型的調度是怎樣做的。
Actor模型實際上可以有成千上萬個,但目前一台通用服務器最多只有24核,當然不可能也開成千上萬個線程。
我們可以把Actor簡單想象成這樣一個類實例:
class Actor { public: void process_1(); void process_2();
void fetch_msg(); private: int actor_id; string actor_name;
list<msg> msg_queue; }
每個Actor定義了自己實現的功能(process_1, process_2).
當msg_queue郵箱有消息到來的時候,就調用fetch_msg去獲取這些消息進行處理。
這一步就是靠調度線程來做了。
Actor模型的調度實現起碼要有:
1. 一個位於主線程的Actor隊列,如 global_queue<Actor*> gq, 當某個Actor收到消息時,就會被放進這個gq,等待工作線程進行調度。
2. n個工作線程,這個就要根據機器的核數來決定開多少個了,例如只是一台雙核的機器,那么開一個就好了,開多了就會浪費時間在線程切換上,得不償失。
每個工作線程做的事件很簡單,向主線程詢問任務,獲取任務,處理任務,然后又繼續詢問,大致如下:
while(true) { task_list = fetch_task(); process_task(task_list); }
所以一個Actor的創建和調度過程如下:
1. 在主線程創建並放入管理列表.
2. 其他actor往本actor發送消息,消息進入msg_queue,本actor進入 global_queue等待調度。
3. 有工作線程處理完一堆任務了,向主線程詢問任務,主線程把本actor分配給這個工作線程。
4. 該工作線程取出msg,調用actor相應處理函數處理這個消息。
所以可見,actor數目與工作線程數目沒有必然的關系,當然理想狀態是,每個actor都有自己的處理線程,這里有消息來到時,就可以馬上處理,不用等待。
理論上,actor開的越多,業務邏輯就分的越細,每次處理的時間就越短,只要actor的數目超過線程數,就可以最大限度利用多核的優勢,cpu的調度就越充分。
所以actor模型設計關鍵在於如何將業務邏輯平攤到更多的actor上,而不是集中。 例如上面提到global_logic是多人玩法的業務邏輯,只要一細分,可以把分成
幫會actor,組隊actor,戰斗actor等等,這樣三個消息同時就有機會被三個cpu處理,而不是固定只有一個。
Actor可以理解成用戶級別的進程,與操作系統級別的進程分離,即使開很多Actor,只要工作線程數目設計合理( <= 系統cpu核數),就能保證線程能在一直同一個cpu上
進行操作,減少線程切換的消耗,這對於cpu核數小的機器非常有用。 而對於像24核的機器,因為開辟的線程數是配置的,所以也很好規划一台機器能部署多少個服。
而多進程如果要對某些功能進行擴展,如增加login,增加logic,就是要增加一個系統線程,一旦進程數超過了cpu核,就會有時間浪費在切換線程上了,
這是一個缺點。
而Actor模型本身是優秀的,但是Actor的調度算法有會有很多種實現,而且必然涉及到鎖的設計,這就需要設計者的設計功力了。