天天看點

【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)
  • 它們的關系如下圖所示:
  • 句柄集:
  • 句柄(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事件并處理客戶請求

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

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

    ,也

    無須

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

    同步

    對請求隊列的

    通路

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

    僅支援一個事件源集合

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

繼續閱讀