天天看點

【Socket】兩種高效事件處理模式&并發模式

兩種高效事件處理模式&并發模式

  • 遊雙-《Linux高性能伺服器程式設計》
  • 本來想做個筆記的,但是發現這塊内容書中很多都感覺是有用的,是以很大篇幅的搬了過來,其中加入了我的了解,并有重點标注。

伺服器程式設計架構

  • 伺服器程式種類繁多,但是基本架構都一樣,​

    ​不同之處在于邏輯處理​

    ​。
  • 下圖所示,伺服器基本架構。該圖既能用來描述​

    ​一台伺服器​

    ​​,也能用來描述​

    ​一個伺服器機群​

    ​。
【Socket】兩種高效事件處理模式&并發模式
  • 各子產品概念
子產品 單個伺服器架構 伺服器機群
I/O邏輯單元 處理客戶連接配接,讀寫網絡資料 作為接入伺服器,實作負載聚恒
邏輯單元 業務程序或線程 邏輯伺服器
網絡存儲單元 本地資料庫、檔案或緩存 資料庫伺服器
請求隊列 各單元之間的通信方式 各伺服器之間的永久TCP連接配接

I/O處理單元

  • I/O處理單元是伺服器​

    ​管理用戶端連接配接的子產品​

    ​。它通常要完成一下工作:
  • 等待并​

    ​接受​

    ​新的客戶連結;
  • ​接收​

    ​客戶資料;
  • 将伺服器響應的資料​

    ​傳回​

    ​給用戶端。
  • 資料的收發不一定在I/O處理單元中執行,也可能在邏輯單元中執行,具體在何處執行取決于事件處理模式。
  • 對于一個伺服器機群來說,I/O處理單元是一個專門的接入伺服器。它實作負載均衡,從所有邏輯伺服器中選取​

    ​負荷最小​

    ​的一台來為新客戶服務。

邏輯單元

  • 一個邏輯單元通常是一個​

    ​程序或線程​

    ​。
  • 它​

    ​分析并處理客戶資料​

    ​​,然後将結果​

    ​傳遞​

    ​​給I/O處理單元或者直接​

    ​發送​

    ​給用戶端。
  • 具體使用哪種方式取決于事件處理模式。
  • 對伺服器機群而言,一個邏輯單元本身就是一台邏輯伺服器。伺服器通常擁有多個邏輯單元, 以實作對多個客戶任務的​

    ​并行​

    ​處理。

網絡存儲單元

  • 網絡存儲單元可以是資料庫、緩存和檔案,甚至是一台獨立的伺服器。
  • 它不是必須的,例如:ssh、telnet等登入伺服器就不需要這個單元。

請求隊列

  • 請求隊列是​

    ​各個單元之間通信方式的抽象​

    ​。
  • I/O處理單元接收到客戶請求時,需要以某種方式來​

    ​通知​

    ​一個邏輯單元來處理該請求。
  • 同樣,多個邏輯單元同時通路一個存儲單元時,也需要采用某種機制來​

    ​協調​

    ​處理競态條件。
  • 請求隊列通常被實作為​

    ​池​

    ​的一部分。
  • 對于伺服器機群而言,請求隊列是各台伺服器之間預先建立的、靜态的、永久的TCP連結。
  • 這種TCP連接配接能​

    ​提高伺服器之間交換資料的效率,因為它避免了動态建立TCP導緻的額外系統開銷​

    ​。

兩種高效的事件處理模式

  • 伺服器通常需要處理三類事件:
  • I/O事件
  • 信号
  • 定時事件
  • 下面介紹兩種高效的事件處理模式: Reactor與Proactor。
  • ​同步I/O模型通常用于實作Reactor模式​

    ​​,​

    ​異步I/O模型則用于實作Proactor模式​

    ​​。可以以使用同步I/O模型去​

    ​模拟​

    ​Proactor模式。

Reactor模式

  • Reactor模式中,​

    ​主線程隻負責監聽檔案描述符上是否有事件發生​

    ​​,有的話就立即将該事件​

    ​通知​

    ​​工作線程(即,邏輯單元,下同)除此之外,主線程​

    ​不做任何其它實質性的工作​

    ​。
  • 讀寫資料,接收新的連接配接,以及處理客戶請求(業務邏輯)均在​

    ​工作線程​

    ​中完成。
  • 使用同步I/O模型(以epoll_wait為例)實作的Reactor模式的工作流程:
  1. 主線程往epoll核心事件表中​

    ​注冊​

    ​socket上的讀就緒事件。(監聽socket與連接配接socket成功建立連接配接後,以下socket都指的是連接配接socket)
  2. 主線程調用epoll_wait​

    ​等待​

    ​socket上有資料可讀。
  3. 當socket上有資料可讀時,epoll_wait​

    ​通知​

    ​​主線程。主線程則将socket可讀事件​

    ​放入​

    ​請求隊列。
  4. 睡眠在請求隊列上的某個工作線程被​

    ​喚醒​

    ​​,它從連接配接socket讀取資料,并​

    ​處理​

    ​客戶請求,然後往epoll核心事件表中注冊該socket上的寫就緒事件。
  5. 主線程調用epoll_wait​

    ​等待​

    ​scoket可寫。
  6. 當socket可寫時,epoll_wait​

    ​通知​

    ​​主線程。主線程将socket可寫事件​

    ​放入​

    ​請求隊列。
  7. 睡眠在請求隊列上的某個工作線程被​

    ​喚醒​

    ​​,它往socket上​

    ​寫入​

    ​伺服器處理客戶請求的結果。
  • 總結: 主線程僅負責監聽socket看是否有發生事件,然後就通知工作線程讀取,處理資料,如為寫事件(即,要應答),再在epoll核心事件表上注冊該連接配接socket的可寫事件,然後再由某個工作線程接管,處理,執行應答,讀事件同理。
  • Reactor模式工作流程圖如下所示:
【Socket】兩種高效事件處理模式&并發模式
  • 工作線程從請求隊列中取出事件後,将​

    ​根據事件的類型決定如何處理​

    ​它。
  • 對于​

    ​可讀​

    ​​事件,執行​

    ​讀資料和處理請求​

    ​的操作;
  • 對于​

    ​可寫​

    ​​事件,執行​

    ​寫資料​

    ​的操作;
  • 是以,如上圖所示的Reactor模式中,沒必要區分所謂的“讀工作線程”和“寫工作線程”。

Proactor模式

  • 與Reactor模式不同,​

    ​Proactor模式将所有I/O操作都交給主線程和核心來處理​

    ​,工作線程僅僅負責業務邏輯。
  • 是以,Proactor模式更符合伺服器基本架構圖中的描述。
  • 使用異步I/O模型(以aio_read和aio_write為例)實作的Proactor模式的工作流程:
  1. 主線程調用aio_read函數向核心​

    ​注冊socket上的讀完成事件​

    ​​,并告訴核心使用者​

    ​讀緩沖區的位置​

    ​​,以及讀操作完成時​

    ​如何通知​

    ​應用程式(這裡以信号為例,詳情請參考sigevent的man手冊)。
  2. 主線程​

    ​繼續處理其它邏輯​

    ​。
  3. 當socket上的資料被​

    ​讀入使用者緩沖區後​

    ​​,核心使用者​

    ​向應用程式發送一個信号​

    ​​,以​

    ​通知應用程式資料已經可用​

    ​。
  4. 應用程式​

    ​預先定義好的信号處理函數選擇一個工作線程來處理客戶請求​

    ​​。工作線程處理完客戶請求之後,調用aio_write函數向核心​

    ​注冊socket上的寫完成事件​

    ​​,并告訴核心使用者​

    ​寫緩沖區的位置​

    ​​,以及寫操完成時​

    ​如何通知​

    ​應用程式(仍以信号為例)。
  • 總結: 核心使用者與主線程進行I/O操作,由信号通知主線程喚醒一個工作線程進行處理資料(業務邏輯),業務處理完,再交給核心使用者與主線程進行I/O操作(伺服器應答)。
  • Proactor模式工作流程圖如下圖所示:
【Socket】兩種高效事件處理模式&并發模式
  • 上圖中,連接配接socket上的讀寫事件是​

    ​通過aio_read/aio_write向核心注冊的​

    ​​,是以​

    ​核心将通過信号來向應用程式報告連接配接socket上的讀寫事件​

    ​。
  • 是以,​

    ​主線程上的epoll_wait調用僅能用來檢測監聽socket上的連接配接請求事件​

    ​,而不能用來檢測連接配接socket上的讀寫事件。

對比&總結

  • Reactor模式與Proactor相對比:
  • I/O操作:
  • Reactor: 工作線程來完成。
  • 對應socket上可以讀(寫)資料了,​

    ​喚醒一個工作線程在對應socket上讀(寫)​

    ​。
  • Proactor: 主線程和核心來完成。
  • 對應socket上可以讀(寫)資料了,​

    ​核心使用者後讀取​

    ​​完成通知應該程式,由定義好的信号處理函數選擇一個​

    ​工作線程進行資料業務處理​

    ​​,然後通知​

    ​核心可以寫回去了​

    ​(伺服器應答)。
  • 資料業務處理:
  • Reactor: 工作線程來完成。
  • Proactor: 工作線程來完成。

模拟Proactor模式

  • 使用同步I/O方式模拟出Proactor模式。
  • 原理是: ​

    ​主線程執行讀寫操作​

    ​​,讀寫完成之後,主線程向工作線程通知這一"完成事件"。從​

    ​工作線程​

    ​​的角度來看,它們就​

    ​直接擷取了資料讀寫的結果​

    ​,接下來要做的隻是對讀寫的結果進行邏輯處理。
  • 使用同步I/O模型(仍以epoll_wait為例)模拟出的Proactor模式的工作流程如下:(其中socket為連接配接socket)
  1. ​主線程​

    ​​往epoll核心事件表中​

    ​注冊socket上的讀就緒事件​

    ​。
  2. ​主線程​

    ​​調用epoll_wait​

    ​等待socket上有資料可讀​

    ​。
  3. 當socket上有資料可讀時,epoll_wait通知主線程。​

    ​主線程​

    ​​從socket上循環​

    ​讀取資料​

    ​​,将讀取到的資料封裝成一個請求對象并​

    ​插入到請求隊列​

    ​中。
  4. 睡眠在請求隊列上的某個​

    ​工作線程被喚醒​

    ​​,它獲得請求對象并​

    ​處理客戶請求​

    ​​,然後往epoll核心表中​

    ​注冊socket上的寫就緒事件​

    ​。
  5. ​主線程​

    ​調用epoll_wait等待socket可讀。
  6. 當socket可寫時,epoll_wait​

    ​通知主線程​

    ​​。​

    ​主線程往socket上寫入伺服器處理客戶請求的結果​

    ​。
  • 總結: 主線程負責I/O操作,工作線程僅負責資料的處理(業務邏輯)。
  • 工作流程如下圖所示:
【Socket】兩種高效事件處理模式&并發模式

兩種高效的并發模式

  • 并發程式設計的目的是讓程式“同時”執行多個任務。
  • 如果程式是​

    ​計算密集型​

    ​​的,并發程式設計并沒有優勢,反而由于任務的​

    ​切換使效率降低​

    ​。
  • 如果程式是​

    ​I/O密集型​

    ​​的,比如經常讀寫檔案,通路資料等,因為I/O操作的速度遠沒有CPU的計算速度快,是以讓程式​

    ​阻塞于I/O操作将浪費大量的CPU時間​

    ​。
  • 如果程式有多個執行線程,則目前被I/O操作所阻塞的執行線程可​

    ​主動放棄​

    ​CPU(由作業系統來排程),并将執行線程轉移到其他線程。
  • 這樣一來,CPU就可以做更加有意義的事情(除非所有線程都同時被I/O操作所阻塞),而不是等待I/O操作完成,進而顯著提升CPU的使用率。
  • 實作上: 并發程式設計主要有​

    ​多程序​

    ​​和​

    ​多線程​

    ​兩種方式。
  • 對于下圖來說,​

    ​并發模式是指I/O處理單元和多個邏輯單元之間協調完成任務的方法​

    ​。
  • 伺服器主要有兩種并發程式設計模式:
  • 半同步/半異步模式(half-sync/half-async)
  • 上司者/追随者模式(Leader/Followers)
【Socket】兩種高效事件處理模式&并發模式

半同步/半異步模式

  • 這裡的半同步/半異步模式中的“同步"與”異步“與I/O模型中的“同步"與”異步“是完全​

    ​不同的概念​

    ​。
  • I/O模型中:
  • “同步"與”異步“區分的是核心向應用程式通知的是​

    ​何種I/O事件​

    ​(是就緒事件還是完成事件);
  • 以及該由​

    ​誰來完成I/O讀寫​

    ​(是應用程式還是核心)。
  • 在并發模式中:
  • "同步"指的是程式完全按照代碼序列的​

    ​順序執行​

    ​;
  • “異步”指的是程式的執行需要由​

    ​系統事件來驅動​

    ​。
  • 常見的系統事件包括中斷、信号等。
  • 下圖a描述了同步的讀操作,下圖b描述了異步的讀操作。
【Socket】兩種高效事件處理模式&并發模式
  • 按照​

    ​同步方式運作的線程稱為同步線程​

    ​​,按照​

    ​異步方式運作的線程稱為異步線程​

    ​。
  • 相比于同步線程,`異步線程的執行效率更高,實時性強。——(異步線程優點)
  • 但編寫以異步方式執行的程式相對​

    ​複雜,難于調試和擴充​

    ​​,而且​

    ​不适合于大量的并發​

    ​。——(異步線程缺點)
  • 同步線程則相反,雖然它​

    ​效率相對較低​

    ​​,​

    ​實時性較差​

    ​​,但是​

    ​邏輯簡單​

    ​。——(同步線程優缺點)
  • 是以,對于像伺服器這種及要求​

    ​較好的實時性​

    ​​,又要求​

    ​能同時處理多個客戶請求​

    ​的應用程式,我們就應該同時使用同步線程與異步線程來實作,即采用半同步/半異步模式來實作。
  • 半同步/半異步模式中:
  • ​同步線程用于處理客戶邏輯,相當于伺服器基本架構圖中的邏輯單元​

    ​;
  • ​異步線程用于處理I/O請求, 相當于伺服器基本架構圖中的I/O處理單元。​

  • 工作線程處理I/O操作,是以半同步/半異步模式采用的是Reactor事件處理模式。
  • ​異步線程監聽​

    ​​到客戶請求後,就将其​

    ​封裝成請求對象并插入請求隊列​

    ​中。請求隊列将​

    ​通知​

    ​某個工作在同步模式的​

    ​工作線程​

    ​來讀取并處理該請求對象。具體選擇哪個工作線程來為新的客戶請求伺服器,則取決于​

    ​請求隊列​

    ​的設計。
  • 比如最簡單的輪流選取工作線程的Round Robin算法,也可以通過條件變量或信号量來随機地選擇一個工作線程。
  • 半同步/半異步模式的工作流程如下圖所示:
【Socket】兩種高效事件處理模式&并發模式

半同步/半反應堆模式

  • 在伺服器程式中,如果結合考慮兩種事件處理模式的幾種I/O模型,則半同步/半異步模式就存在多種​

    ​變體​

    ​。
  • 其中一種就叫做半同步/半反應堆模式(half-sync/half-reactive),如下圖所示:
  • half-reactive展現在工作線程讀寫連接配接socket上的資料,詳見下面。
【Socket】兩種高效事件處理模式&并發模式
  • 如上圖所示,​

    ​異步線程隻有一個,由主線程來充當​

    ​​,它負責​

    ​監聽所有socket上的事件​

    ​。
  • 如果​

    ​監聽socket​

    ​​上有​

    ​可讀事件發生​

    ​​(監聽socket在服務端當然隻能發生可讀事件,哪有自己給自己發消息的,即在監聽socket上寫),即有新的連接配接到來,主線程就​

    ​接受以得到新的連接配接socket​

    ​​,然後往epoll核心事件表中​

    ​注冊​

    ​該socket的讀寫事件。
  • 如果​

    ​連接配接socket​

    ​​上有​

    ​讀寫事件發生​

    ​​,即有新的客戶請求到來或有資料要發送至用戶端,主線程就将該​

    ​連接配接socket插入請求隊列​

    ​中。
  • 所有​

    ​工作線程都睡眠在請求隊列上​

    ​​,當有任務到來時,它們将通過​

    ​競争​

    ​​(比如申請互斥鎖)來獲得任務的​

    ​接管權​

    ​​。這種競争機制使得隻有​

    ​空閑​

    ​​的工作線程才有機會來​

    ​處理新任務​

    ​,這是很合理的。
  • 上圖中,主線程插入請求中隊列中的任務是​

    ​就緒的連接配接socket​

    ​(即,該連接配接socket上有讀寫事件發生)。
  • 這說明該圖所示的半同步/半反應堆模式采用的事件處理模式是​

    ​Reactor模式​

    ​​,它要求​

    ​工作線程自己從socket上讀取客戶資料和往socket上寫入伺服器應答​

    ​。
  • 這就是其名字(half-reactive)的含義。
  • 半同步/半反應堆也可以​

    ​模拟Proactor事件處理模式​

    ​​,即由​

    ​主線程完成資料的讀寫​

    ​。在這種情況下:
  • ​主線程​

    ​​一般會将應用程式資料、任務類型等資訊​

    ​封裝成一個任務對象​

    ​(即把對應socket上的資料讀出來,封裝到一個任務對象中);
  • 然後将其(或者指向該任務對象的一個指針)​

    ​插入請求隊列​

    ​;
  • ​工作線程​

    ​​從請求隊列中​

    ​取得​

    ​​任務對象之後,即可​

    ​處理​

    ​之,無需讀寫操作。
  • 半同步/半反應堆模式存在如下缺點:
  • ​主線程和工作線程共享請求隊列​

    ​。
  • 主線程往請求隊列中添加任務,或者工作線程從請求隊列中取出任務,都需要對請求隊列加鎖保護,進而白白耗費CPU時間。
  • ​每個工作線程在同一時間隻能處理一個客戶請求​

    ​。
  • 如果客戶數量較多,而工作線程較少,則請求隊列中獎​

    ​堆積很多任務對象,用戶端的響應速度将越來越慢​

    ​。
  • 如果通過增加工作線程來解決這一問題,則工作線程的​

    ​切換也将耗費大量CPU時間​

    ​。

相對高效的半同步/半異步模式

  • 下圖描述了一種相對高效的半同步/半異步模式,它的​

    ​每個工作線程都能同時處理多個客戶連接配接​

    ​。
【Socket】兩種高效事件處理模式&并發模式
  • 上圖中:
  • ​主線程隻管理監聽socket,連接配接socket由工作線程來管理​

    ​。
  • 當有新的連接配接到來時,主線程就接受之并将​

    ​新傳回的連接配接socket派發給某個工作線程​

    ​。
  • 此後​

    ​該連接配接socket上的任何I/O操作都由被選中的工作線程​

    ​來處理,直到客戶關閉連接配接。
  • 主線程派發socket的最簡單的方式,是往它和工作線程之間的​

    ​管道​

    ​裡寫資料。
  • 工作線程檢測到​

    ​管道上有資料可讀時​

    ​,就分析是否是一個新的客戶連接配接到來。
  • 如果是,則把該新的連接配接socket上的讀寫事件注冊到自己的epoll核心事件表中。
  • ​以後該連接配接socket上的所有I/O事件都由此工作線程進行監聽與操作,直到客戶關閉連接配接。​

  • (與上面重複了,這裡再寫一遍我想印象會深一些。)
  • 如上圖所示,每個線程(主線程與工作線程)都維持自己的事件循環,它們各自獨立地監聽不同的事件。
  • 是以,在這種高效的半同步/半異步模式中,​

    ​每個線程都工作在異步模式​

    ​,是以它并非嚴格意義上的半同步/半異步模式。

上司者/追随者模式

  • 上司者/追随者模式是​

    ​多個工作線程輪流獲得事件源集合,輪流監聽、分發并處理事件​

    ​的一種模式。
  • 在任意時間點,程式都​

    ​僅有一個上司者線程,它負責監聽I/O事件。​

  • 其它線程都是追随者,它們休眠線上程池中等待成為新的上司者。
  • 目前的上司者如果​

    ​檢測到I/O事件​

    ​​,首先要從線程池中​

    ​推選出新的上司者線程​

    ​​,​

    ​然後處理I/O事件​

    ​。
  • 此時,​

    ​新的上司者等待新的I/O事件​

    ​​,而​

    ​原來的上司者則處理目前檢測到的I/O事件​

    ​,二者實作了并發。
  • 上司者/追随者模式包含如下幾個元件:
  • 句柄集(HandleSet)
  • 線程集(ThreadSet)
  • 事件處理器(EventHandler)
  • 具體的事件處理器(ConcreteEventHandler)
  • 它們的關系如下圖所示:
【Socket】兩種高效事件處理模式&并發模式
  • 句柄集:
  • 句柄(Handle)用于表示​

    ​I/O資源​

    ​​,在Linux下通常就是一個​

    ​檔案描述符​

    ​。
  • 句柄集管理衆多句柄,它使用wait_for_event方法來​

    ​監聽​

    ​​這些句柄上的I/O事件,并将其中的​

    ​就緒事件通知給上司者線程​

    ​。
  • 上司者線程調用綁定到Handle上的事件處理器來處理事件。
  • 上司者将Handle和事件處理器綁定是通過調用句柄集中的register_handle方法實作的。
  • 線程集:
  • ​線程集是所有工作線程(包括上司者線程和追随者線程)的管理者​

    ​。
  • 它負責各個線程之間的​

    ​同步​

    ​​,以及新上司者線程的​

    ​推選​

    ​。
  • 線程集中的線程在任一時間必處于如下​

    ​三種狀态之一​

    ​;
  • Leader: 線程單目前處于​

    ​上司者身份​

    ​​,負責​

    ​等待​

    ​句柄集上的I/O事件。
  • Processing: 線程​

    ​正在處理事件​

    ​。
  • 上司者檢測到I/O事件之後,可以​

    ​轉移​

    ​​到Processing狀态來處理事件,并調用promote_new_leader方法​

    ​推選​

    ​​新的上司者;也可以​

    ​指定​

    ​​其他追随者來​

    ​處理​

    ​​事件(Event Handoff),此時上司者的地位​

    ​不變​

    ​。
  • 當處于Processing狀态的線程處理完事件之後,如果目前線程集中​

    ​沒有​

    ​​上司者,則它将​

    ​成為新的上司者​

    ​​,否則它就​

    ​直接轉變為追随者​

    ​。
  • Follower: 線程目前處于​

    ​追随者身份​

    ​​,通過調用線程集的join方法​

    ​等待成為新的上司者​

    ​​,也可能被目前的上司者​

    ​指定來處理新的任務​

    ​。
  • 如下圖所示這三種狀态之間的轉換關系:
  • 需要注意的是,上司者線程推選新的上司者和追随者等待成為新的上司者,這兩個操作都将修改線程集,是以線程集提供一個成員Synchronizer來同步這兩個操作,以​

    ​避免競态​

    ​條件。
  • 事件處理器和具體的事件處理器:
  • 事件處理器通常包含一個過多個回調函數(handle_event)。這些​

    ​回調函數用于處理事件對應的業務邏輯​

    ​。
  • 事件處理器在使用前需要被​

    ​綁定到某個句柄上​

    ​​,當該句柄上​

    ​有事件發生時​

    ​​,上司者就​

    ​執行與之綁定的事件處理器中的回調函數​

    ​。
  • 具體的事件處理器是事件處理器的派生類,它們必須​

    ​重新實作基類handle_event方法,以處理特定的任務​

    ​。
  • 綜上所述,上司者/追随者工作流程如下圖所示:
  • 由于​

    ​上司者線程自己監聽I/O事件并處理客戶請求​

    ​​,因而上司者/追随者模式​

    ​不需要線上程之間傳遞任何額外的資料​

    ​​,也​

    ​無須​

    ​​像半同步/半反應堆那樣線上程之間​

    ​同步​

    ​​對請求隊列的​

    ​通路​

    ​。
  • 但上司者/追随者的一個明顯缺點是​

    ​僅支援一個事件源集合​

    ​,是以也無法像高效的半同步/半異步模式那樣,讓每個工作線程獨立地管理多個客戶連結。