Linux网络编程 | 并发模式:半同步/半异步模式、领导者/追随者模式
文章目錄
- 同步與異步
- 半同步/半異步模式
- 變體:半同步/半反應堆模式
- 改進:更高效的半同步/半異步模式
- 領導者/追隨者模式
- 組件 :句柄集、線程集、事件處理器
并發模式是指I/O處理單元和多個邏輯單元之間協調完成任務的方法。服務器主要有兩種并發編程模式:半同步/半異步模式(half-sync/half-async)模式和領導者/追隨者(Leader/Followers)模式
同步與異步
首先在這里要指出,這里的"同步"和“異步”與I/O模型中的”同步I/O“與“異步I/O”是兩個完全不同的概念,不能將其混淆。
在I/O模型中,”同步I/O“和”異步I/O“主要的區別就是內核向應用程序通知的是哪種I/O事件(就緒事件/完成事件),以及由誰來完成I/O的讀寫操作(應用程序/內核)。
- 同步I/O:內核向應用程序通知就緒事件,由應用程序自身來完成I/O的讀寫操作
- 異步I/O:由內核來完成I/O的讀寫后向應用程序通知完成事件
在并發模式中,”同步和”異步“主要的區別就是功能完成的流程是否是順序化的,是否需要等待
- 同步:當遇到阻塞任務的時候,就會一直等到到處理完成,程序完全按照代碼序列的順序執行,
- 異步:程序的執行需要由系統事件來驅動,當遇到任務的使用會發起一個系統事件進行處理,此時繼續處理其他邏輯,而系統處理完成后則會觸發事件返回結果,此時程序的執行流程是不確定的,沒有順序上的要求。
如下面兩張圖就分別是同步與異步下的讀操作。
同步 異步按照同步方式運行的線程又稱為同步線程,按照異步方式運行的線程被稱為異步線程。
很顯然異步線程的執行效率更高,實時性也強,但是由于編寫異步方式執行的程序相對復雜,難以進行調試和拓展,并且不適合于大量的并發。
而同步線程恰恰相反,雖然它的效率相對來說較低,并且實時性差,但是由于其邏輯簡單,所以可維護性和拓展性高。
對于服務器來說,我們既需要較好的實時性,又要求能夠同時處理多個客戶的請求,我們就需要將兩者互補,同時使用同步線程和異步線程,即采用半同步/半異步模式來實現
半同步/半異步模式
在半同步/半異步模式中,同步線程用于處理客戶邏輯,異步線程用于處理I/O事件。
當異步線程監聽到客戶請求后,讀取socket中的數據,將其封裝成請求對象并插入請求隊列中。請求隊列將通知某個同步線程來獲取請求對象并進行業務邏輯的處理。
具體選擇哪個同步線程主要取決于請求隊列的設計,如輪流選取的Round Robin算法,或使用條件變量、信號量等來隨機選取。
下圖就是半同步/半異步模式的工作流程。
變體:半同步/半反應堆模式
如果結合之前所講的事件處理模式以及I/O模型,半同步/半異步模式就會存在很多種變體,其中一種比較具有代表性的變體就是半同步/半反應堆模式(half-sync/half-reactive)。
其工作流程如下圖
在圖中,異步線程只有主線程一個,它負責監聽所有socket上的事件。如果當前有新的連接到來,主線程就會連接socket,并且往epoll內核事件表中注冊該socket上的讀寫事件。如果epoll監控集合中有讀寫事件就緒,此時就會將socket放入請求隊列中。當請求隊列中有任務到來時,所有同步線程就會去競爭(比如申請互斥鎖)獲得任務的管轄權。這種競爭機制使得只有空閑的工作線程才有機會來處理新任務,避免了忙閑不均的情況。
從上面我們可以看到,這種由主線程進行監聽,工作線程來完成I/O以及業務邏輯的事件處理模式正是Reactor模式(反應器模式),這也就是名字中半反應堆(half-reactive)的由來。
當然半同步/半反應堆模式也可以使用同步I/O模型模擬的Proactor模式,即由主線程來完成數據的讀寫,然后將任務以及程序數據封裝為任務對象,放入請求隊列中。工作線程從請求隊列中取得任務對象之后即可直接處理,不需要進行讀寫操作。
半同步/半反應堆模式的缺點:
- 主線程和工作線程共享請求隊列,主線程往請求隊列中添加任務,工作線程從請求隊列中取出任務是互斥的操作,需要對請求隊列進行加鎖保護,導致白白浪費CPU時間
- 每個工作線程在同一時間內只能處理一個客戶請求,如果客戶較多而工作線程較少,就會導致任務隊列中堆積大量的任務對象,此時客戶端的響應將越來越慢。而如果通過增加工作線程來解決這個問題,就會因為大量的上下文切換導致消耗大量的CPU時間。
改進:更高效的半同步/半異步模式
在前面所描述的幾種模式中我們每個工作線程都只能同時處理一個連接,為了改進這一點,就有了下圖這種每個工作線程可以同時處理多個客戶連接(每個工作線程維護一個epoll) 的改進版本。
在圖中,主線程只負責管理用于監聽的socket,而新連接的socket派發給工作線程來管理。
所以每當有新連接到來時,主線程就會接收連接,并將新返回的連接socket派發給某個工作線程,之后在該socket上的任何I/O操作都由被選中的工作線程進行管理,直到客戶關閉連接。
主線程向工作線程派發socket最簡單的方式就是往它和工作線程之間的管道里寫入數據。如果工作線程檢測到管道中有數據可讀,就會判斷是否是一個新的客戶連接請求到來,如果是,則把該socket上的讀寫事件注冊到自己的內核事件表中。
從圖中可以看出,在該模式下每個線程(主線程和工作線程)都維護了自己的事件循環(epoll),它們各自獨立地監聽不同的事件,每個線程都工作在異步模式,因此它并非嚴格意義上的半同步/半異步模式。
領導者/追隨者模式
領導者/追隨者模式是多個工作線程輪流獲得事件源集合,輪流進行監聽、分發并處理事件者。的一種模式。
在任意時間點中,程序都只會有一個領導者線程,它負責進行I/O事件的監聽。而其他的線程則為追隨者線程,他們會休眠在線程池中,等待成為新的領導者。
如果當前的領導者檢測到I/O事件,他就會在線程池中推選出新的領導者線程,然后自己去處理I/O事件。此時前領導者去進行I/O事件的處理,而新領導者則會繼續等待新的I/O事件的到來,以此完成并發。
用通俗點的方法來講就像是一群在營地中輪流放哨的哨兵,每次都會有一個人在值班,而其他人去休息。當值班者發現有什么特殊情況的時候就會去讓領班叫醒一個哨兵來繼續放哨,然后自己去探查情況。如果探查情況完后沒人值班,則自己繼續盯梢,否則就去休息。
組件 :句柄集、線程集、事件處理器
領導者/追隨者模式包含的組件有:句柄集(HandleSet)、線程集(ThreadSet)、事件處理器(EventHandler),它們之間的關系如圖所示。
句柄集
句柄用于表示I/O資源,在Linux下通常就是一個文件描述符。句柄集其實就是句柄的監控集合,通過調用wait_for_event方法來監聽這些句柄上的I/O事件,并將其中的就緒事件通知給領導者線程,而領導者線程則調用綁定到Handle上的事件處理器來完成事件的處理。
線程集
線程集是所有工作線程(包括領導者和追隨者)的管理者。它負責各線程之間的同步,以及新領導者線程的推選。
線程集中的線程在任意時間都必然處于下面三種狀態之一
- 領導者(Leader)
線程此時處于領導者身份,負責監聽句柄集上的I/O事件 - 事件處理中(Processing)
此時線程正在處理事件。領導者檢測到I/O事件后,轉移到Processing狀態進行事件的處理,并且調用promote_new_leader()讓線程集推選出新的領導者。如果不想讓出領導者的地位,也可以指定其他的追隨者來處理事件。
當處于Processing狀態的線程處理完事件之后,如果當前線程集中沒有領導者,他就會成為新的領導者,否則就會變回追隨者。 - 追隨者(Follower)
線程此時處于追隨者身份,此時處于休眠狀態,通過調用線程集中的join()等待被推選為新的領導者,也可能被當前的領導者指定處理新的任務。
狀態轉移圖如下
事件處理器
事件處理器通常包含一個或者多個回調函數handle_event。通過這些回調函數來處理事件對應的業務邏輯。
事件處理器在使用前首先需要被綁定到某個句柄之上,每當該句柄上有事件發生的時候,領導者就執行與之綁定的事件處理器中的回調函數。
通過這幾種組件以及特性,領導者/追隨者模式的工作流程如上圖。
由于領導者線程自己負責監聽I/O事件并且自己處理客戶請求,所以在領導者/追隨者模式中不需要再線程之間傳遞任何額外的數據,也不需要像半同步/半反應堆模式那樣在線程之間同步對請求隊列的訪問。
但是領導者/追隨者模式有一個明顯的缺點就是只能支持一個事件源集合,因此無法讓每個工作線程獨立地管理多個客戶連接。
總結
以上是生活随笔為你收集整理的Linux网络编程 | 并发模式:半同步/半异步模式、领导者/追随者模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux网络编程 | 事件处理模式:R
- 下一篇: Linux网络编程 | 信号 :信号函数