目錄:
《I/O模型之二:Linux IO模式及 select、poll、epoll詳解》
《I/O模型之三:兩種高性能 I/O 設計模式 Reactor 和 Proactor》
Reactor(反應堆)和Proactor(前攝器)
《I/O模型之三:兩種高性能 I/O 設計模式 Reactor 和 Proactor》
《【轉】第8章 前攝器(Proactor):用於為異步事件多路分離和分派處理器的對象行為模式》
《Java NIO系列教程(八)JDK AIO編程》-- java AIO的proactor模式
《Java NIO系列教程(七) selector原理 Epoll版的Selector》--java NIO的Reactor模式
平時接觸的開源產品如Redis、ACE,事件模型都使用的Reactor模式;而同樣做事件處理的Proactor,由於操作系統的原因,相關的開源產品也少;這里學習下其模型結構,重點對比下兩者的異同點;Reactor 和 Proactor 是基於事件驅動,在網絡編程中經常用到兩種設計模式。
說到異步IO,其實現在很難實現真正的異步,大部分情況下仍然需要阻塞在某個多路復用函數,比如select 或者 epoll 上,得到就緒描述符,然后調用注冊在相應描述符上的回調函數。這種方式是現在的反應堆設計的基本思路。我截取一段反應堆模型的圖給大家看看。
這個圖是截取至 python的 twisted 服務器的反應堆文章介紹,但是大致和我們需要的理念一樣。
事件循環阻塞查看描述符是否就緒,當就緒后返回可讀或可寫的描述符,也有可能帶外數據或者出錯等情況。
因為 select 很多文章都介紹了,下面我就以 epoll 為例,貌似是2.4.6還是哪個版本以后加入的IO多路復用方式。
epoll 較select 的一些優點就不多說了,內核采用紅黑樹機制,大大提高了epoll 的性能。著名的 libevent Nginx等內部都采用這個機制。
曾經在一個項目中用到了網絡庫 libevent,也學習了一段時間,其內部實現所用到的就是 Reactor,所知道的還有 ACE;Proactor 模式的庫有 Boost.Asio,ACE,暫時沒有用過。但我也翻閱了一些文檔,理解了它的實現方法。下面是我在學習這兩種設計模式過程的筆記。
反應器Reactor
意圖
在事件驅動的應用中,將一個或多個客戶的服務請求分離(demultiplex)和事件分發器 (dispatch)給應用程序。
上下文
在事件驅動的應用中,同步地、有序地處理同時接收的多個服務請求。
問題
在分布式系統尤其是服務器這一類事件驅動應用中,雖然這些請求最終會被序列化地處理,但是必須時刻准備着處理多個同時到來的服務請求。在實際應用中,這些請求總是通過一個事件(如CONNECTOR、READ、WRITE等)來表示的。在有 序地處理這些服務請求之前,應用程序必須先分離和調度這些同時到達的事件。為了有效地解決這個問題,我們需要做到以下4方面:
為了提高系統的可測量性和反應時間,應用程序不能長時間阻塞在某個事件源上而停止對其他事件的處理,這樣會嚴重降低對客戶端的響應度。
為了提高吞吐量,任何沒有必要的上下文切換、同步和CPU之間的數據移動都要避免。
引進新的服務或 改良已有的服務都要對既有的事件分離和調度機制帶來盡可能小的影響。
大量的應用程序代碼需要隱藏在復雜的多線程和同步機制之后。
解決方案
在一個或多個事件源上等待事件的到來,例如,一個已經連接的Socket描述符就是一個事件源。將事件的分離和調度整合到處理它的服務中,而將分離和調度機制從應用程序對特定事件的處理中分離開,也就是說分離和調度機制與特定的應用程序無關。
具體來說,每個應用程序提供的每個服務都有一個獨立的事件處理器與之對應。由 事件處理器處理來自事件源的特定類型的事件。每個事件處理器都事先注冊到Reactor管理器中。Reactor管理器使用同步事件分離器在一個或多個事件源中等待事件的發生。當事件發生后,同步事件分離器通知Reactor管理器,最后由Reactor管理器調度和該事件相關的事件處理器來完成請求的服務。
結構
在Reactor模式中,有5個關鍵的參與者。
描述符(handle):
由操作系統提供,用於識別每一個事件,如Socket描述符、文 件描述符等。在Linux中,它用一個整數來表示。事件可以來自外部,如來自客戶端 的連接請求、數據等。事件也可以來自內部,如定時器事件。
同步事件分離器 (demultiplexer):
是一個函數,用來等待一個或多個事件的發生。調用者會被阻 塞,直到分離器分離的描述符集上有事件發生。Linux的select函數是一個經常被使 用的分離器。
事件處理器接口(event handler):
是由一個或多個模板函數組成的接口。這些模板函數描述了和應用程序相關的對某個事件的操作。具體的事件處理器:是事件處理器接口的實現。它實現了應用程序提供的某個服務。每個具體的事件處理器總和一個描述符相關。它使用描述符來識別事件、識別應用程序提供的服務。
Reactor管理器(reactor):
定義了一些接口,用於應用程序控制事件調度,以及應用程序注冊、刪除事件處理器和相關的描述符。它是事件處理器的調度核心。Reactor管理器使用同步事件分離器來等待事件的發生。一旦事件發生,Reactor管理器先是分離每個事件,然后調度事件處理器,最后調用相關的模板函 數來處理這個事件。通過上述分析,我們注意到,是Reactor管理器而不是應用程序負責等待事件、分離事件和調度事件。實際上,Reactor管理器並沒有被具體的事件處理器調用,而是管理器調度具體的事件處理器,由事件處理器對發生的事件做出處理。這就是類似Hollywood原則的“反向控制”。應用程序要做的僅僅是實現一個具體的事件處理器,然后把它注冊到Reactor管理器中。接下來的工作由管理 器來完成。這些參與者的相互關系如圖2-1所示。
注意:這里提及的反應堆模型,實際上就是外國人設計的一個概念,將我們從面向過程編程轉換為一個面向對象編程的一個東西,我們可以簡單的認為,直接操作IO模型,是一個面向過程的操作,而由一個反應堆來操作的,是一個面向對象的操作,期間,面相對象操作會提高部分性能。
Reactor模式結構
Reactor包含如下角色:
- Handle 句柄;用來標識socket連接或是打開文件;
- Synchronous Event Demultiplexer:同步事件多路分解器:由操作系統內核實現的一個函數;用於阻塞等待發生在句柄集合上的一個或多個事件;(如select/epoll;)
- Event Handler:事件處理接口
- Concrete Event HandlerA:實現應用程序所提供的特定事件處理邏輯;
- Reactor:反應器,定義一個接口,實現以下功能:
1)供應用程序注冊和刪除關注的事件句柄;
2)運行事件循環;
3)有就緒事件到來時,分發事件到之前注冊的回調函數上處理;
“反應”器名字中”反應“的由來:
“反應”即“倒置”,“控制逆轉”
具體事件處理程序不調用反應器,而是由反應器分配一個具體事件處理程序,具體事件處理程序對某個指定的事件發生做出反應;這種控制逆轉又稱為“好萊塢法則”(不要調用我,讓我來調用你)
業務流程及時序圖
- 應用啟動,將關注的事件handle注冊到Reactor中;
- 調用Reactor,進入無限事件循環,等待注冊的事件到來;
- 事件到來,select返回,Reactor將事件分發到之前注冊的回調函數中處理;
Proactor模式
Proactor模式的類圖如上圖所示,Proactor模式又叫前攝器或主動器模式。它用於實現異步I/O模型,運行流程如下:
1. Initiator主動調用Asynchronous Operation Processor發起異步I/O操作,
2. 記錄異步操作的參數和函數地址放入完成事件隊列(Completion Event Queue)中
3. Proactor循環檢測異步事件是否完成。如果完成則從完成事件隊列中取出回調函數完成回調。
Boost庫中的asio就使用了Proactor模式,其底層的異步I/O由操作系統提供,而異步事件的分發還是由epoll/kequeue/select等實現。
Proactor模式結構
Proactor主動器模式包含如下角色
- Handle 句柄;用來標識socket連接或是打開文件;
- Asynchronous Operation Processor:異步操作處理器;負責執行異步操作,一般由操作系統內核實現;
- Asynchronous Operation:異步操作
- Completion Event Queue:完成事件隊列;異步操作完成的結果放到隊列中等待后續使用
- Proactor:主動器;為應用程序進程提供事件循環;從完成事件隊列中取出異步操作的結果,分發調用相應的后續處理邏輯;
- Completion Handler:完成事件接口;一般是由回調函數組成的接口;
- Concrete Completion Handler:完成事件處理邏輯;實現接口定義特定的應用處理邏輯;
業務流程及時序圖
- 應用程序啟動,調用異步操作處理器提供的異步操作接口函數,調用之后應用程序和異步操作處理就獨立運行;應用程序可以調用新的異步操作,而其它操作可以並發進行;
- 應用程序啟動Proactor主動器,進行無限的事件循環,等待完成事件到來;
- 異步操作處理器執行異步操作,完成后將結果放入到完成事件隊列;
- 主動器從完成事件隊列中取出結果,分發到相應的完成事件回調函數處理邏輯中;
對比兩者的區別
主動和被動
以主動寫為例:
- Reactor將handle放到select(),等待可寫就緒,然后調用write()寫入數據;寫完處理后續邏輯;
- Proactor調用aoi_write后立刻返回,由內核負責寫操作,寫完后調用相應的回調函數處理后續邏輯;
可以看出,Reactor被動的等待指示事件的到來並做出反應;它有一個等待的過程,做什么都要先放入到監聽事件集合中等待handler可用時再進行操作;
Proactor直接調用異步讀寫操作,調用完后立刻返回;
實現
Reactor實現了一個被動的事件分離和分發模型,服務等待請求事件的到來,再通過不受間斷的同步處理事件,從而做出反應;
Proactor實現了一個主動的事件分離和分發模型;這種設計允許多個任務並發的執行,從而提高吞吐量;並可執行耗時長的任務(各個任務間互不影響)
優點
Reactor實現相對簡單,對於耗時短的處理場景處理高效;
操作系統可以在多個事件源上等待,並且避免了多線程編程相關的性能開銷和編程復雜性;
事件的串行化對應用是透明的,可以順序的同步執行而不需要加鎖;
事務分離:將與應用無關的多路分解和分配機制和與應用相關的回調函數分離開來,
Proactor性能更高,能夠處理耗時長的並發場景;
缺點
Reactor處理耗時長的操作會造成事件分發的阻塞,影響到后續事件的處理;
Proactor實現邏輯復雜;依賴操作系統對異步的支持,目前實現了純異步操作的操作系統少,實現優秀的如windows IOCP,但由於其windows系統用於服務器的局限性,目前應用范圍較小;而Unix/Linux系統對純異步的支持有限,應用事件驅動的主流還是通過select/epoll來實現;
適用場景
Reactor:同時接收多個服務請求,並且依次同步的處理它們的事件驅動程序;
Proactor:異步接收和同時處理多個服務請求的事件驅動程序;
總結
相比網絡編程中最簡單的思路模式:bind,listen,accept,read,server operator,write,Reactor 和 Proactor 是兩種高性能的設計模式,掌握此兩種模式,有助於理解一些網絡庫的工作流程。此文提到了兩種設計模式,但沒有一些技術細節,譬如多線程同步。如果在 Reactor 中支持多線程,或多個線程共享一個 Proactor,線程的同步問題就來了。
《Comparing Two High-Performance I/O Design Patterns》提到一個將 Reactor 模擬 Proactor 而不借助操作系統異步機制的方法:同樣在 Reactor 注冊感興趣的事件(比如讀),當事件發生時,執行非阻塞的讀,讀畢即才調用數據處理——假異步。