兩種高效事件處理模式&并發模式
- 遊雙-《Linux高性能伺服器程式設計》
- 本來想做個筆記的,但是發現這塊内容書中很多都感覺是有用的,是以很大篇幅的搬了過來,其中加入了我的了解,并有重點标注。
伺服器程式設計架構
- 伺服器程式種類繁多,但是基本架構都一樣,
。
不同之處在于邏輯處理
- 下圖所示,伺服器基本架構。該圖既能用來描述
,也能用來描述
一台伺服器
。
一個伺服器機群
- 各子產品概念
子產品 | 單個伺服器架構 | 伺服器機群 |
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模型去
異步I/O模型則用于實作Proactor模式
Proactor模式。
模拟
Reactor模式
- Reactor模式中,
,有的話就立即将該事件
主線程隻負責監聽檔案描述符上是否有事件發生
工作線程(即,邏輯單元,下同)除此之外,主線程
通知
。
不做任何其它實質性的工作
- 讀寫資料,接收新的連接配接,以及處理客戶請求(業務邏輯)均在
中完成。
工作線程
- 使用同步I/O模型(以epoll_wait為例)實作的Reactor模式的工作流程:
- 主線程往epoll核心事件表中
socket上的讀就緒事件。(監聽socket與連接配接socket成功建立連接配接後,以下socket都指的是連接配接socket)
注冊
- 主線程調用epoll_wait
socket上有資料可讀。
等待
- 當socket上有資料可讀時,epoll_wait
主線程。主線程則将socket可讀事件
通知
請求隊列。
放入
- 睡眠在請求隊列上的某個工作線程被
,它從連接配接socket讀取資料,并
喚醒
客戶請求,然後往epoll核心事件表中注冊該socket上的寫就緒事件。
處理
- 主線程調用epoll_wait
scoket可寫。
等待
- 當socket可寫時,epoll_wait
主線程。主線程将socket可寫事件
通知
請求隊列。
放入
- 睡眠在請求隊列上的某個工作線程被
,它往socket上
喚醒
伺服器處理客戶請求的結果。
寫入
- 總結: 主線程僅負責監聽socket看是否有發生事件,然後就通知工作線程讀取,處理資料,如為寫事件(即,要應答),再在epoll核心事件表上注冊該連接配接socket的可寫事件,然後再由某個工作線程接管,處理,執行應答,讀事件同理。
- Reactor模式工作流程圖如下所示:
- 工作線程從請求隊列中取出事件後,将
它。
根據事件的類型決定如何處理
- 對于
事件,執行
可讀
的操作;
讀資料和處理請求
- 對于
事件,執行
可寫
的操作;
寫資料
- 是以,如上圖所示的Reactor模式中,沒必要區分所謂的“讀工作線程”和“寫工作線程”。
Proactor模式
- 與Reactor模式不同,
,工作線程僅僅負責業務邏輯。
Proactor模式将所有I/O操作都交給主線程和核心來處理
- 是以,Proactor模式更符合伺服器基本架構圖中的描述。
- 使用異步I/O模型(以aio_read和aio_write為例)實作的Proactor模式的工作流程:
- 主線程調用aio_read函數向核心
,并告訴核心使用者
注冊socket上的讀完成事件
,以及讀操作完成時
讀緩沖區的位置
應用程式(這裡以信号為例,詳情請參考sigevent的man手冊)。
如何通知
- 主線程
。
繼續處理其它邏輯
- 當socket上的資料被
,核心使用者
讀入使用者緩沖區後
,以
向應用程式發送一個信号
。
通知應用程式資料已經可用
- 應用程式
。工作線程處理完客戶請求之後,調用aio_write函數向核心
預先定義好的信号處理函數選擇一個工作線程來處理客戶請求
,并告訴核心使用者
注冊socket上的寫完成事件
,以及寫操完成時
寫緩沖區的位置
應用程式(仍以信号為例)。
如何通知
- 總結: 核心使用者與主線程進行I/O操作,由信号通知主線程喚醒一個工作線程進行處理資料(業務邏輯),業務處理完,再交給核心使用者與主線程進行I/O操作(伺服器應答)。
- Proactor模式工作流程圖如下圖所示:
- 上圖中,連接配接socket上的讀寫事件是
,是以
通過aio_read/aio_write向核心注冊的
。
核心将通過信号來向應用程式報告連接配接socket上的讀寫事件
- 是以,
,而不能用來檢測連接配接socket上的讀寫事件。
主線程上的epoll_wait調用僅能用來檢測監聽socket上的連接配接請求事件
對比&總結
- Reactor模式與Proactor相對比:
- I/O操作:
- Reactor: 工作線程來完成。
- 對應socket上可以讀(寫)資料了,
。
喚醒一個工作線程在對應socket上讀(寫)
- Proactor: 主線程和核心來完成。
- 對應socket上可以讀(寫)資料了,
完成通知應該程式,由定義好的信号處理函數選擇一個
核心使用者後讀取
,然後通知
工作線程進行資料業務處理
(伺服器應答)。
核心可以寫回去了
- 資料業務處理:
- Reactor: 工作線程來完成。
- Proactor: 工作線程來完成。
模拟Proactor模式
- 使用同步I/O方式模拟出Proactor模式。
- 原理是:
,讀寫完成之後,主線程向工作線程通知這一"完成事件"。從
主線程執行讀寫操作
的角度來看,它們就
工作線程
,接下來要做的隻是對讀寫的結果進行邏輯處理。
直接擷取了資料讀寫的結果
- 使用同步I/O模型(仍以epoll_wait為例)模拟出的Proactor模式的工作流程如下:(其中socket為連接配接socket)
-
往epoll核心事件表中
主線程
。
注冊socket上的讀就緒事件
-
調用epoll_wait
主線程
。
等待socket上有資料可讀
- 當socket上有資料可讀時,epoll_wait通知主線程。
從socket上循環
主線程
,将讀取到的資料封裝成一個請求對象并
讀取資料
中。
插入到請求隊列
- 睡眠在請求隊列上的某個
,它獲得請求對象并
工作線程被喚醒
,然後往epoll核心表中
處理客戶請求
。
注冊socket上的寫就緒事件
-
調用epoll_wait等待socket可讀。
主線程
- 當socket可寫時,epoll_wait
。
通知主線程
。
主線程往socket上寫入伺服器處理客戶請求的結果
- 總結: 主線程負責I/O操作,工作線程僅負責資料的處理(業務邏輯)。
- 工作流程如下圖所示:
兩種高效的并發模式
- 并發程式設計的目的是讓程式“同時”執行多個任務。
- 如果程式是
的,并發程式設計并沒有優勢,反而由于任務的
計算密集型
。
切換使效率降低
- 如果程式是
的,比如經常讀寫檔案,通路資料等,因為I/O操作的速度遠沒有CPU的計算速度快,是以讓程式
I/O密集型
。
阻塞于I/O操作将浪費大量的CPU時間
- 如果程式有多個執行線程,則目前被I/O操作所阻塞的執行線程可
CPU(由作業系統來排程),并将執行線程轉移到其他線程。
主動放棄
- 這樣一來,CPU就可以做更加有意義的事情(除非所有線程都同時被I/O操作所阻塞),而不是等待I/O操作完成,進而顯著提升CPU的使用率。
- 實作上: 并發程式設計主要有
和
多程序
兩種方式。
多線程
- 對于下圖來說,
。
并發模式是指I/O處理單元和多個邏輯單元之間協調完成任務的方法
- 伺服器主要有兩種并發程式設計模式:
- 半同步/半異步模式(half-sync/half-async)
- 上司者/追随者模式(Leader/Followers)
半同步/半異步模式
- 這裡的半同步/半異步模式中的“同步"與”異步“與I/O模型中的“同步"與”異步“是完全
。
不同的概念
- I/O模型中:
- “同步"與”異步“區分的是核心向應用程式通知的是
(是就緒事件還是完成事件);
何種I/O事件
- 以及該由
(是應用程式還是核心)。
誰來完成I/O讀寫
- 在并發模式中:
- "同步"指的是程式完全按照代碼序列的
;
順序執行
- “異步”指的是程式的執行需要由
。
系統事件來驅動
- 常見的系統事件包括中斷、信号等。
- 下圖a描述了同步的讀操作,下圖b描述了異步的讀操作。
- 按照
,按照
同步方式運作的線程稱為同步線程
。
異步方式運作的線程稱為異步線程
- 相比于同步線程,`異步線程的執行效率更高,實時性強。——(異步線程優點)
- 但編寫以異步方式執行的程式相對
,而且
複雜,難于調試和擴充
。——(異步線程缺點)
不适合于大量的并發
- 同步線程則相反,雖然它
,
效率相對較低
,但是
實時性較差
。——(同步線程優缺點)
邏輯簡單
- 是以,對于像伺服器這種及要求
,又要求
較好的實時性
的應用程式,我們就應該同時使用同步線程與異步線程來實作,即采用半同步/半異步模式來實作。
能同時處理多個客戶請求
- 半同步/半異步模式中:
-
;
同步線程用于處理客戶邏輯,相當于伺服器基本架構圖中的邏輯單元
-
異步線程用于處理I/O請求, 相當于伺服器基本架構圖中的I/O處理單元。
- 工作線程處理I/O操作,是以半同步/半異步模式采用的是Reactor事件處理模式。
-
到客戶請求後,就将其
異步線程監聽
中。請求隊列将
封裝成請求對象并插入請求隊列
某個工作在同步模式的
通知
來讀取并處理該請求對象。具體選擇哪個工作線程來為新的客戶請求伺服器,則取決于
工作線程
的設計。
請求隊列
- 比如最簡單的輪流選取工作線程的Round Robin算法,也可以通過條件變量或信号量來随機地選擇一個工作線程。
- 半同步/半異步模式的工作流程如下圖所示:
半同步/半反應堆模式
- 在伺服器程式中,如果結合考慮兩種事件處理模式的幾種I/O模型,則半同步/半異步模式就存在多種
。
變體
- 其中一種就叫做半同步/半反應堆模式(half-sync/half-reactive),如下圖所示:
- half-reactive展現在工作線程讀寫連接配接socket上的資料,詳見下面。
- 如上圖所示,
,它負責
異步線程隻有一個,由主線程來充當
。
監聽所有socket上的事件
- 如果
上有
監聽socket
(監聽socket在服務端當然隻能發生可讀事件,哪有自己給自己發消息的,即在監聽socket上寫),即有新的連接配接到來,主線程就
可讀事件發生
,然後往epoll核心事件表中
接受以得到新的連接配接socket
該socket的讀寫事件。
注冊
- 如果
上有
連接配接socket
,即有新的客戶請求到來或有資料要發送至用戶端,主線程就将該
讀寫事件發生
中。
連接配接socket插入請求隊列
- 所有
,當有任務到來時,它們将通過
工作線程都睡眠在請求隊列上
(比如申請互斥鎖)來獲得任務的
競争
。這種競争機制使得隻有
接管權
的工作線程才有機會來
空閑
,這是很合理的。
處理新任務
- 上圖中,主線程插入請求中隊列中的任務是
(即,該連接配接socket上有讀寫事件發生)。
就緒的連接配接socket
- 這說明該圖所示的半同步/半反應堆模式采用的事件處理模式是
,它要求
Reactor模式
。
工作線程自己從socket上讀取客戶資料和往socket上寫入伺服器應答
- 這就是其名字(half-reactive)的含義。
- 半同步/半反應堆也可以
,即由
模拟Proactor事件處理模式
。在這種情況下:
主線程完成資料的讀寫
-
一般會将應用程式資料、任務類型等資訊
主線程
(即把對應socket上的資料讀出來,封裝到一個任務對象中);
封裝成一個任務對象
- 然後将其(或者指向該任務對象的一個指針)
;
插入請求隊列
-
從請求隊列中
工作線程
任務對象之後,即可
取得
之,無需讀寫操作。
處理
- 半同步/半反應堆模式存在如下缺點:
-
。
主線程和工作線程共享請求隊列
- 主線程往請求隊列中添加任務,或者工作線程從請求隊列中取出任務,都需要對請求隊列加鎖保護,進而白白耗費CPU時間。
-
。
每個工作線程在同一時間隻能處理一個客戶請求
- 如果客戶數量較多,而工作線程較少,則請求隊列中獎
。
堆積很多任務對象,用戶端的響應速度将越來越慢
- 如果通過增加工作線程來解決這一問題,則工作線程的
。
切換也将耗費大量CPU時間
相對高效的半同步/半異步模式
- 下圖描述了一種相對高效的半同步/半異步模式,它的
。
每個工作線程都能同時處理多個客戶連接配接
- 上圖中:
-
。
主線程隻管理監聽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)用于表示
,在Linux下通常就是一個
I/O資源
。
檔案描述符
- 句柄集管理衆多句柄,它使用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事件并處理客戶請求
,也
不需要線上程之間傳遞任何額外的資料
像半同步/半反應堆那樣線上程之間
無須
對請求隊列的
同步
。
通路
- 但上司者/追随者的一個明顯缺點是
,是以也無法像高效的半同步/半異步模式那樣,讓每個工作線程獨立地管理多個客戶連結。
僅支援一個事件源集合