兩種高效的併發模式
前言
我們都知道,併發程式設計的目的是讓程式“同時”執行多個任務,提高效率。當一個程式是計算密集型的時,併發程式設計並沒有優勢,反而由於任務的切換時效率降低。但是,當一個程式是IO密集型時,採用併發程式設計會極大地提高cpu的利用率。因為IO操作的速度遠遠小於cpu的計算速度,所以讓程式阻塞與IO操作上會浪費大量的CPU時間。而併發程式設計可以讓阻塞於IO操作的執行緒主動放棄CPU,將執行權轉移到其他執行緒。
下面,我們主要來講一下併發模式,即IO處理單元和多個邏輯單元之間協調完成任務的方法。而在伺服器上,主要有兩種併發模式。一種是:半同步/半非同步模式(half-sync/half-async)和領導者-追隨者模式(Leader/Followers)。
簡單認識
關於這兩個模式有兩個很形象的比喻:
半同步/半非同步(half-sync/half-async)
許多餐廳使用 半同步/半非同步 模式的變體。例如,餐廳常常僱傭一個領班負責迎接顧客,並在餐廳繁忙時留意給顧客安排桌位,為等待就餐的顧客按序排隊是必要的。領班由所有顧客“共享”,不能被任何特定顧客佔用太多時間。當顧客在一張桌子入坐後,有一個侍應生專門為這張桌子服務。
領導者/追隨者(Leader/Followers)
在日常生活中,領導者/追隨者模式用於管理許多飛機場計程車候車臺。在該用例中,計程車扮演“執行緒”角色,排在第一輛的計程車成為領導者,剩下的計程車成為追隨者。同樣,到達計程車候車臺的乘客構成了必須被多路分解給出租車的事件,一般以先進先出排序。一般來說,如果任何計程車可以為任何顧客服務,該場景就主要相當於非繫結控制代碼/執行緒關聯。然而,如果僅僅是某些計程車可以為某些乘客服務,該場景就相當於繫結控制代碼/執行緒關聯。
半同步/半非同步模式(half-sync/half-async)
併發模式中,同步與非同步的概念
併發程式設計中的同步與非同步的概念和IO模型中的完全不同。
在IO模型中,同步和非同步區分的是核心嚮應用程序通知的是就緒事件(同步)還是完成事件(非同步);
在併發模式中,同步指的是,完全按照程式碼的順序執行;非同步指的是,程式的執行需要系統事件來驅動,比如:中斷或訊號。
如下圖:a 是同步讀 b是非同步讀
原因
很明顯,非同步執行緒(按非同步方式執行的執行緒)的執行效率高,實時性強,但是相對複雜,不適合大量併發;而同步執行緒(按同步方式執行的執行緒)雖然講效率不高,但是邏輯簡單。所以,對於伺服器來說既要有高併發,實時性還要好的來說,應該同時使用同步執行緒和非同步執行緒,即半同步/半非同步模式。
這個模式中,高層使用同步I/O模型,簡化程式設計。低層使用非同步I/O模型,高效執行。在”複雜度”和”執行效率”之間達到一種平衡。
應用場景
1 、一個系統中的程序有下面的特徵:
①、系統必須響應和處理外部非同步發生的事件,
②、如果為每一個外部資源的事件分派一個獨立的執行緒同步處理I/O,效率很低。
③、如果上層的任務以同步方式處理I/O,實現起來簡單。
2、 一個或多個任務必須在單獨的控制執行緒中執行,其它任務可以在多執行緒中執行:
①、上層的任務(如:資料庫查詢,檔案傳輸)使用同步I/O模型,簡化了編寫並行程式的難度。
②、底層的任務(如網路控制器的中斷處理)使用非同步I/O模型,提供了執行效率。
一般情況下,上層的任務要比下層的任務多,使用一個簡單的層次實現非同步處理的複雜性,可以對外隱藏非同步處理的細節。另外,同步層次和非同步層次任務間的通訊使用一個佇列來協調。
實現方案
主要包含三個層次:非同步任務層、同步任務層和請求佇列層。
Half-sync/Half-async(半同步/半非同步)模式的核心思想是如何將系統中的任務進行恰當的分解,使各個子任務落入合適的層次中。低階的任務或者耗時較短的任務可以安排在非同步任務層。而高階的任務或者耗時較長的任務可以安排在同步任務層。而非同步任務層和同步任務層這兩層之間的協作通過請求佇列層進行解耦:請求佇列層負責非同步任務層和同步任務層之間的資料交換。
工作流程
①、同步執行緒用於處理客戶邏輯;
②、非同步執行緒用於處理IO事件;
③、非同步事件監聽到客戶的請求之後,將其封裝成請求物件並插入請求佇列中。
④、請求佇列將通知某個工作在同步模式的工作執行緒來讀取並處理請求物件。
工作流程圖
半同步/ 半反應堆(half-sync / half-reactive)
在伺服器程式中,如果結合考慮兩種事件處理模型,和集中IO模型,則半同步/ 半非同步模型有多種變種。其中有一種是半同步/半反應堆模型。
非同步執行緒只有一個,由主執行緒充當,負責監聽所有socket事件。如果監聽socket上有可讀事件發生,指的是新的連結請求到來,那麼非同步執行緒接受它,往epoll核心事件表中註冊該socket上的讀寫事件。如果連線socket上有讀寫事件發生,要麼是新的客戶請求,要麼時有資料要傳送給客戶端,主執行緒就把該連線socket插入請求佇列。所有的工作執行緒睡眠在請求佇列上,有任務來的時候,空閒執行緒競爭,獲取任務接管權。
缺點:
1 、主執行緒和工作執行緒共享一個請求佇列,因此當佇列中的任務有變更,就需要加鎖保護,浪費CPU資源。
2 、每一個工作執行緒在同一時間只能處理一個客戶請求,對於客戶數眾多,但是工作任務少的情況下,請求佇列很多工堆積,客戶的響應速度越來越慢。若通過增加工作執行緒來解決的話,工作執行緒的切換也將浪費大量CPU時間。
高效的半同步/半非同步模式
主線成只管監聽socket,連線socket有工作執行緒來管理。當有新的連線到來時,主執行緒就接受並將新返回的連線socket派發給某個工作執行緒,由該工作執行緒負責socket上的所有IO操作,知道客戶端關閉。
可見,每個執行緒都維護著自己的事件迴圈,各自監聽不同的事件。因此,在這種高效的模式中,每個執行緒都工作在非同步模式,並非是嚴格的半同步/半非同步模式。
領導者/追隨者模式(Leader-Follower)
工作方式
領導者/追隨者模式是多個工作執行緒輪流獲得事件源集合,輪流監聽、分發並處理事件的一種模式。在任意時間點,程式都僅有一個領導者執行緒,它負責監聽IO事件。而其他執行緒都是追隨者,它們休眠線上程池中等待成為新的領導者。當前的領導者如果檢測到IO事件,首先要從執行緒池中推選出新的領導者執行緒,然後處理IO事件。此時,新的領導者等待新的IO事件,而原來的領導者則處理IO事件,二者實現了併發。
元件
包含如下幾個元件:
控制代碼集(HandleSet)
執行緒集(ThreadSet)
事件處理器(EventHandler)
具體的事件處理器(ConcreteEventHandler)
控制代碼集
控制代碼表示IO資源,linux下通常是檔案描述符。控制代碼集使用wait_for_event方法監聽這些控制代碼上的IO事件,並將其中的就緒事件通知給領導者執行緒。領導者呼叫繫結到Handle上的事件處理器來處理事件。繫結是通過控制代碼集的register_handle方法實現的。
執行緒集
所有工作執行緒的管理者,負責執行緒同步、推選新領導。執行緒在任一時間必處於以下三種狀態之一:
Leader:領導者執行緒,負責等待控制代碼集上的IO事件。
Processing:執行緒正在處理事件。領導者檢測到IO事件後可以轉移至Processing狀態處理該事件,並呼叫promote_new_leader方法推選新領導者;也可以指定其他追隨者來處理事件,此時領導者地位不變。當處於Processing狀態的執行緒處理完事件後,如果當前執行緒集中沒有領導者,則它將成為新領導者,否則它直接轉為追隨者。
Follower:執行緒處於追隨者身份,通過呼叫執行緒集的join方法等待成為新領導者,也可能被領導者指定來處理新的事件。
這三種狀態之間的轉換關係圖:
注意:領導者推選新領導和追隨者等待成為新領導這兩個操作都會修改執行緒集,因此執行緒集提供一個Synchronizer來同步,避免竟態條件。
事件處理器和具體的事件處理器
事件處理器通常包含一個或多個回撥函式handle_event。這些回撥函式用於處理事件對應的業務邏輯。事件處理器在使用前需要被繫結到某個控制代碼上,當該控制代碼有事件發生時,領導者就執行繫結的事件處理器的回撥函式。具體的事件處理器是事件處理器的派生類。它們重新實現基類的handle_event方法,以處理特定的任務。
領導者/追隨者模式的工作流程圖
優缺點
領導者/追隨者模式最大的優點在於,它是自己監聽I/O事件並處理客戶請求,也就是說從接收到處理都是在同一執行緒中完成,所以不需要線上程之間傳遞任何額外的資料,也不用像半同步/半反應堆模式那樣線上程間同步對請求佇列的訪問。但是它也有明顯的缺點,就是隻支援一種事件源集合,所以導致它不能讓每個執行緒獨立的管理多個客戶連線。
參考:linxu高效能伺服器程式設計