一、並發編程與並發模式
並發編程主要是為了讓程序同時執行多個任務,並發編程對計算精密型沒有優勢,反而由於任務的切換使得效率變低。如果程序是IO精密型的,則由於IO操作遠沒有CPU的計算速度快,所以讓程序阻塞於IO操作將浪費大量的CPU時間。如果程序有多個線程,則當前被IO操作阻塞的線程可主動放棄CPU,將執行權轉給其它線程。(*IO精密型和cpu精密型可以參考此文:CPU-bound(計算密集型) 和I/O bound(I/O密集型))
並發編程主要有多線程和多進程,這里我們先討論並發模式,並發模式指:IO處理單元和多個邏輯直接協調完成任務的方法。服務器主要有兩種並發編程模式:
- 半同步/半異步模式(half-sync/half-async)
- 領導者/追隨者模式(Leader/Followers)
二、半同步/半異步模式(half-sync/half-async)
這里的“同步”和“異步”和“IO”的“同步”“異步”是完全不同的概念。在IO模型中,“同步”和“異步”區分的是內核向應用程序通知的是何種IO事件(是就緒事件還是完成事件),以及該由誰來完成IO讀寫(是應用程序還是內核)。在並發模式中,“同步”指的是程序完全按照代碼序列的順序執行;“異步”指的是程序的執行需要由系統事件來驅動。常見的系統事件包括中斷、信號等。
下圖1描述了並發模式同步讀操作(圖1a)和異步讀操作(圖1b)
圖1並發模式同步讀(a)和異步讀(b)
已同步方式運行的線程為同步線程,異步方式運行的為異步線性,異步線程的執行效率高,實時性強,但編寫異步方式執行的程序相對復雜,難於調試和擴展,而且不適合於大量的並發。同步線程則相反,它雖然效率相對較低,實時性較差,但邏輯簡單。
因此對應服務器要求實時性及同時處理多個請求的程序,可以同時使用同步線程和異步線程即采用半同步/半異步模式。同步線程用於處理客戶邏輯,異步線程用於處理IO事件。異步線程監聽到客戶請求后,就將其封裝成請求對象並插入到請求隊列中。請求隊列將通知某個工作在同步模式的工作線程來讀取並處理該請求對象。具體哪個線性處理取決於請求隊列的設計。下圖2為半同步/半異步的工作流程
圖2半同步/半異步的工作流程
在半同步/半異步模式可以變體成為半同步/半反應堆(half-sync/half-reactive),如下圖3
圖3半同步/半反應堆模式
半同步/半反應堆中,異步線程只有一個,即主線程,他負責監聽所有事件,有事件發生則將事件插入請求隊列中。工作線程休眠在請求隊列中,當任務到來時,通過競爭獲取任務處理權
在上圖3半同步/半反應堆中,主線程插入工作隊列的為就緒的連接socket,他要求工作線程自己socket讀取數據和往socket寫入服務器應答,所有可以看作Reactor模式。實際也可以模擬為Proactor模式,即主線程完成數據的讀寫,將數據封裝成任務對象插入請求隊列,工作線程從請求隊列取出任務對象處理。(Reactor模式和Reactor模式可以參考此文:服務器兩種高效的事件處理模式)
半同步半反應堆模式存在如下缺點:
1、主線程和工作線程共享請求隊列,對請求隊列的操作需求加鎖,耗費CPU時間。
2、每一個工作線程在同一時間只能處理一個客戶請求。客戶數量多,工作線程少,請求隊列任務堆積,響應滿,如果添加試圖通過增加線程則,由於線程切換導致的CPU時間消耗。
這里我們再介紹一種高效的半同步/半異步模式:每個工作線程都能同時處理多個客戶連接。
圖4 高效半同步/半異步模式
主線程只管理監聽socket,連接socket由工作線程來管理。當有新的連接到來時,主線程就接受之並將新返回的連接socket派發給某個工作線程,此后該socket上的任何IO操作都由被選中的工作線程來處理,直到客戶端關閉連接。主線程向工作線程派發socket的最簡單的方式,是往它和工作線程之間的管道里寫數據。工作線程檢測到管道里有數據可讀時,就分析是否是一個新的客戶連接請求到來。如果是,則把該新socket上的讀寫事件注冊到自己的epoll內核事件表中。
每個線程(主線程和工作線程)都維持自己的事件循環,它們各自獨立的監聽不同的事件。因此在這種模式中,每個線程都工作在異步模式,所以它並非嚴格意義上的半同步半異步模式。
三、領導者/追隨者模式(Leader/Followers)
領導者/追隨者模式是多個工作線程輪流獲得事件源集合,輪流監聽、分發並處理事件的一種模式。在任意時間點,程序都僅有一個領導者線程,它負責監聽IO事件。而其他線程都是追隨者,它們休眠在線程池中等待成為新的領導者。當前的領導者如果檢測到IO事件,首先要從線程池中推選出新的領導者線程,然后處理IO事件。此時,新的領導者等待新的IO事件,而原來的領導者則處理IO事件,二者實現了並發。
包含如下幾個組件:
- 句柄集(HandleSet)
- 線程集(ThreadSet)
- 事件處理器(EventHandler)
- 具體的事件處理器(ConcreteEventHandler)。
關系如下圖5
圖5 領導者/追隨者模式的組件
1、句柄集
句柄表示IO資源,linux下通常是文件描述符。句柄集使用wait_for_event方法監聽這些句柄上的IO事件,並將其中的就緒事件通知給領導者線程。領導者調用綁定到Handle上的事件處理器來處理事件。綁定是通過句柄集的register_handle方法實現的。
2、線程集
所有工作線程的管理者,負責線程同步、推選新領導。線程在任一時間必處於以下三種狀態之一:
- Leader:領導者線程,負責等待句柄集上的IO事件。
- Processing:線程正在處理事件。領導者檢測到IO事件后可以轉移至Processing狀態處理該事件,並調用promote_new_leader方法推選新領導者;也可以指定其他追隨者來處理事件,此時領導者地位不變。當處於Processing狀態的線程處理完事件后,如果當前線程集中沒有領導者,則它將成為新領導者,否則它直接轉為追隨者。
- Follower:線程處於追隨者身份,通過調用線程集的join方法等待成為新領導者,也可能被領導者指定來處理新的事件。
這三種狀態之間的轉換關系圖如下圖6:
圖6 領導者/追隨者模式的狀態轉移
注意,領導者推選新領導和追隨者等待成為新領導這兩個操作都會修改線程集,因此線程集提供一個Synchronizer來同步。
3、事件處理器和具體的事件處理器
事件處理器通常包含一個或多個回調函數handle_event。這些回調函數用於處理事件對應的業務邏輯。事件處理器在使用前需要被綁定到某個句柄上,當該句柄有事件發生時,領導者就執行綁定的事件處理器的回調函數。具體的事件處理器是事件處理器的派生類。它們重新實現基類的handle_event方法,以處理特定的任務。
由於領導者自己監聽IO事件並處理客戶請求,該模式不需要在線程間傳遞額外數據,也無需像半同步/半反應堆模式那樣在線程間同步對請求隊列的訪問。但是,該模式的明顯缺點是僅支持一個事件源集合,因此也無法讓每個工作線程獨立管理多個客戶連接。
我們將領導者/追隨者模式的工作流程總結如下圖7
圖7 領導者/追隨者模式的工作流程
注(本文內容參考 Linux高性能服務器編程——第八章 游雙著)