Reactor 和 Proactor 是基於事件驅動,在網絡編程中經常用到兩種設計模式。
曾經在一個項目中用到了網絡庫 libevent,也學習了一段時間,其內部實現所用到的就是 Reactor,所知道的還有 ACE;Proactor 模式的庫有 Boost.Asio,ACE,暫時沒有用過。但我也翻閱了一些文檔,理解了它的實現方法。下面是我在學習這兩種設計模式過程的筆記。
Reactor
Reactor,即反應堆。Reactor 的一般工作過程是首先在 Reactor 中注冊(Reactor)感興趣事件,並在注冊時候指定某個已定義的回調函數(callback);當客戶端發送請求時,在 Reactor 中會觸發剛才注冊的事件,並調用對應的處理函數。在這一個處理回調函數中,一般會有數據接收、處理、回復請求等操作。

libevent 采用的就是 Reactor 的設計思想。其 Reactor 的中心思想是眾所周知的 I/O 多路復用:select,poll,epoll,kqueue 等.libevent 精彩的將定時事件,信號處理,I/O 事件結合在在一起,也就是說用戶同時在 Reactor 中注冊上述三類事件。遺憾的是,libevent 不支持多線程,也就是說它同步處理請求,導致不能處理大量的請求;這樣並不是說 Reactor 實現的網絡庫都不支持多線程,而是 libevent 本身的原因,我們也可以通過修改讓 ilbevent 支持多線程,並發處理多個請求。
下面是 libevent 的一段代碼,大概能夠說明 Reactor 工作模式:
/*accept callback function.*/
void accept_callback(int fd,
short ev,void *arg)
{
......
}
......
struct event accept_event;
event_set(&accept_event,
socketlisten,
EV_READ|EV_PERSIST,
accept_callback,
NULL);
event_add(&accept_event,
NULL);
event_dispatch();
Proactor
從上面 Reactor 模式中,發現服務端數據的接收和發送都占用了用戶狀態(還有一種內核態),這樣服務器的處理操作就在數據的讀寫上阻塞花費了時間,節省這些時間的辦法是借助操作系統的異步讀寫;異步讀寫在調用的時候可以傳遞回調函數或者回送信號,當異步操作完畢,內核會自動調用回調函數或者發送信號。Proactor 就是這么做的,所以很依賴操作系統。來一幅 UML:
和時序圖:
注:這兩幅美艷的圖片來自 Proactor.doc,下面會提到.
Proactor 的實現主要有三個部分:異步操作處理器,Proactor 和 事件處理函數。其中:
- 異步操作處理器,很依賴操作系統的異步處理機制,如若操作系統沒有實現,我們可以自行模擬,即開專門的數據讀寫線程,數據讀寫完畢觸發相應的時間(如果有注冊的話); - Proactor,會接收異步操作的提醒,調用相應的事件處理函數,它有自己的 event loop; - 事件處理函數,事件觸發,執行操作;
曾經看過 Proactor.doc,作者是 Douglas C. Schmidt,你可以在這里閱讀此文檔。里面的關於 Proactor 的講解很精彩,部分摘抄和自己的理解如下:當連接 web 服務器時:
- web 服務器指定(1)接收器,此接收器相當於服務器的客戶端,它可以啟動異步的 accept 操作;
- 接收器調用操作系統上的異步接收操作(2),並傳遞自己和 Proactor 的引用;異步接收操作結束后,前者用作事件處理函數,后者會回過頭來分發事件;注:傳遞 Proactor 是為了讓操作系統通知正確的 Proactor,可能會存在多個 Proactor;傳遞接收器自己是為了在異步接收操作結束后 Proactor 能調用正確的事件處理函數,以下同理。
- web 服務器調用 Proactor 的事件循環;(3)
- web 瀏覽器連接 web 服務器;(4)
- 異步接收操作結束后,操作系統產生事件(通過回調或者信號)並通知 Proactor(5),Proactor 收到后會調用相應的事件處理函數,即交由接收器處理;(6)
- 接收器生成 HTTP 處理器,執行操作;(7)
- HTTP 處理器解析事件,啟動異步讀操作(8),獲取來自瀏覽器的 GET 請求。同樣,HTTP 處理器傳遞自己和 Proactor 的引用;
- web 服務器的控制權交還回 Proactor 的事件循環。(9)
接收 GET 請求過后,會處理數據:
- 瀏覽器發送(1)一個 HTTP GET 請求;
- 異步讀操作結束后,操作系統會通知 Proactor,Proactor 分發給事件處理函數;(2,3)
- 事件處理器解析請求。(4)2-4 步驟會重復,指導所有的數據都接收為止;
- 事件處理器產生答復數據;(5)
- HTTP 處理器啟動異步寫操作(6),傳輸應答數據,同樣的這里還會傳遞處理器自己和 Proactor;
- 異步寫操作結束,操作系統通知 Proactor(7),Proactor 分發給事件處理函數(8)。6-8 步驟會重復直到所有的數據寫完為止。至此,一個請求回復完成。
總結
相比網絡編程中最簡單的思路模式:bind,listen,accept,read,server operator,write,Reactor 和 Proactor 是兩種高性能的設計模式,掌握此兩種模式,有助於理解一些網絡庫的工作流程。此文提到了兩種設計模式,但沒有一些技術細節,譬如多線程同步。如果在 Reactor 中支持多線程,或多個線程共享一個 Proactor,線程的同步問題就來了。共享一篇印象筆記關於線程的綜合討論:這里.
《Comparing Two High-Performance I/O Design Patterns》提到一個將 Reactor 模擬 Proactor 而不借助操作系統異步機制的方法:同樣在 Reactor 注冊感興趣的事件(比如讀),當事件發生時,執行非阻塞的讀,讀畢即才調用數據處理——假異步。
最后,實踐出真知。歡迎討論。
參考:
- Proactor.pdf,http://www.laputan.org/pub/sag/proactor.pdf
- 《Comparing Two High-Performance I/O Design Patterns》,http://www.artima.com/articles/io_design_patterns.html
- 《libevent源碼深度剖析》,http://blog.csdn.net/sparkliang/article/details/4957667
- 《 IO - 同步,異步,阻塞,非阻塞 (亡羊補牢篇)》,http://blog.csdn.net/historyasamirror/article/details/5778378
搗亂 2013-08-21




