轉一篇:Reactor模式

轉載自:http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html

 

前記

第一次聽到Reactor模式是三年前的某個晚上,一個室友忽然跑過來問我什麼是Reactor模式?我上網查了一下,不少人都是給出NIO中的 Selector的例子,並且就是NIO裏Selector多路複用模型,只是給它起了一個比較fancy的名字而已,雖然它引入了EventLoop概 念,這對我來講是新的概念,可是代碼實現倒是同樣的,於是我並無很在乎這個模式。然而最近開始讀Netty源碼,而Reactor模式是不少介紹Netty的文章中被大肆宣傳的模式,於是我再次問本身,什麼是Reactor模式?本文就是對這個問題關於個人一些理解和嘗試着來解答。

html

什麼是Reactor模式

要回答這個問題,首先固然是求助Google或Wikipedia,其中Wikipedia上說:「The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.」。從這個描述中,咱們知道Reactor模式首先是事件驅動的,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers;這個Service Handler會同步的將輸入的請求(Event)多路複用的分發給相應的Request Handler。若是用圖來表達:

從結構上,這有點相似生產者消費者模式,即有一個或多個生產者將事件放入一個Queue中,而一個或多個消費者主動的從這個Queue中Poll事件來處理;而Reactor模式則並無Queue來作緩衝,每當一個Event輸入到Service Handler以後,該Service Handler會主動的根據不一樣的Event類型將其分發給對應的Request Handler來處理。

更學術的,這篇文章(Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events)上說:「The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. Also known as Dispatcher, Notifier」。這段描述和Wikipedia上的描述相似,有多個輸入源,有多個不一樣的EventHandler(RequestHandler)來處理不一樣的請求,Initiation Dispatcher用於管理EventHander,EventHandler首先要註冊到Initiation Dispatcher中,而後Initiation Dispatcher根據輸入的Event分發給註冊的EventHandler;然而Initiation Dispatcher並不監聽Event的到來,這個工做交給Synchronous Event Demultiplexer來處理。java

Reactor模式結構

在解決了什麼是Reactor模式後,咱們來看看Reactor模式是由什麼模塊構成。圖是一種比較簡潔形象的表現方式,於是先上一張圖來表達各個模塊的名稱和他們之間的關係:

Handle:即操做系統中的句柄,是對資源在操做系統層面上的一種抽象,它能夠是打開的文件、一個鏈接(Socket)、Timer等。因爲Reactor模式通常使用在網絡編程中,於是這裏通常指Socket Handle,即一個網絡鏈接(Connection,在Java NIO中的Channel)。這個Channel註冊到Synchronous Event Demultiplexer中,以監聽Handle中發生的事件,對ServerSocketChannnel能夠是CONNECT事件,對SocketChannel能夠是READ、WRITE、CLOSE事件等。
Synchronous Event Demultiplexer:阻塞等待一系列的Handle中的事件到來,若是阻塞等待返回,即表示在返回的Handle中能夠不阻塞的執行返回的事件類型。這個模塊通常使用操做系統的select來實現。在Java NIO中用Selector來封裝,當Selector.select()返回時,能夠調用Selector的selectedKeys()方法獲取Set<SelectionKey>,一個SelectionKey表達一個有事件發生的Channel以及該Channel上的事件類型。上圖的「Synchronous Event Demultiplexer ---notifies--> Handle」的流程若是是對的,那內部實現應該是select()方法在事件到來後會先設置Handle的狀態,而後返回。不瞭解內部實現機制,於是保留原圖。
Initiation Dispatcher:用於管理Event Handler,即EventHandler的容器,用以註冊、移除EventHandler等;另外,它還做爲Reactor模式的入口調用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的Event Handler處理,即回調EventHandler中的handle_event()方法。
Event Handler:定義事件處理方法:handle_event(),以供InitiationDispatcher回調使用。
Concrete Event Handler:事件EventHandler接口,實現特定事件處理邏輯。react

Reactor模式模塊之間的交互

簡單描述一下Reactor各個模塊之間的交互流程,先從序列圖開始:

1. 初始化InitiationDispatcher,並初始化一個Handle到EventHandler的Map。
2. 註冊EventHandler到InitiationDispatcher中,每一個EventHandler包含對相應Handle的引用,從而創建Handle到EventHandler的映射(Map)。
3. 調用InitiationDispatcher的handle_events()方法以啓動Event Loop。在Event Loop中,調用select()方法(Synchronous Event Demultiplexer)阻塞等待Event發生。
4. 當某個或某些Handle的Event發生後,select()方法返回,InitiationDispatcher根據返回的Handle找到註冊的EventHandler,並回調該EventHandler的handle_events()方法。
5. 在EventHandler的handle_events()方法中還能夠向InitiationDispatcher中註冊新的Eventhandler,好比對AcceptorEventHandler來,當有新的client鏈接時,它會產生新的EventHandler以處理新的鏈接,並註冊到InitiationDispatcher中。數據庫

Reactor模式實現

Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中,一直以Logging Server來分析Reactor模式,這個Logging Server的實現徹底遵循這裏對Reactor描述,於是放在這裏以作參考。Logging Server中的Reactor模式實現分兩個部分:Client鏈接到Logging Server和Client向Logging Server寫Log。於是對它的描述分紅這兩個步驟。
Client鏈接到Logging Server

1. Logging Server註冊LoggingAcceptor到InitiationDispatcher。
2. Logging Server調用InitiationDispatcher的handle_events()方法啓動。
3. InitiationDispatcher內部調用select()方法(Synchronous Event Demultiplexer),阻塞等待Client鏈接。
4. Client鏈接到Logging Server。
5. InitiationDisptcher中的select()方法返回,並通知LoggingAcceptor有新的鏈接到來。 
6. LoggingAcceptor調用accept方法accept這個新鏈接。
7. LoggingAcceptor建立新的LoggingHandler。
8. 新的LoggingHandler註冊到InitiationDispatcher中(同時也註冊到Synchonous Event Demultiplexer中),等待Client發起寫log請求。
Client向Logging Server寫Log

1. Client發送log到Logging server。
2. InitiationDispatcher監測到相應的Handle中有事件發生,返回阻塞等待,根據返回的Handle找到LoggingHandler,並回調LoggingHandler中的handle_event()方法。
3. LoggingHandler中的handle_event()方法中讀取Handle中的log信息。
4. 將接收到的log寫入到日誌文件、數據庫等設備中。
3.4步驟循環直到當前日誌處理完成。
5. 返回到InitiationDispatcher等待下一第二天志寫請求。

Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events有對Reactor模式的C++的實現版本,多年不用C++,於是略過。 編程

Java NIO對Reactor的實現

在Java的NIO中,對Reactor模式有無縫的支持,即便用Selector類封裝了操做系統提供的Synchronous Event Demultiplexer功能。這個Doug Lea已經在Scalable IO In Java中有很是深刻的解釋了,於是再也不贅述,另外這篇文章對Doug Lea的Scalable IO In Java有一些簡單解釋,至少它的代碼格式比Doug Lea的PPT要整潔一些。

須要指出的是,不一樣這裏使用InitiationDispatcher來管理EventHandler,在Doug Lea的版本中使用SelectionKey中的Attachment來存儲對應的EventHandler,於是不須要註冊EventHandler這個步驟,或者設置Attachment就是這裏的註冊。並且在這篇文章中,Doug Lea從單線程的Reactor、Acceptor、Handler實現這個模式出發;演化爲將Handler中的處理邏輯多線程化,實現相似Proactor模式,此時全部的IO操做仍是單線程的,於是再演化出一個Main Reactor來處理CONNECT事件(Acceptor),而多個Sub Reactor來處理READ、WRITE等事件(Handler),這些Sub Reactor能夠分別再本身的線程中執行,從而IO操做也多線程化。這個最後一個模型正是Netty中使用的模型。而且在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events的9.5 Determine the Number of Initiation Dispatchers in an Application中也有相應的描述。緩存

EventHandler接口定義

對EventHandler的定義有兩種設計思路:single-method設計和multi-method設計:
A single-method interface:它將Event封裝成一個Event Object,EventHandler只定義一個handle_event(Event event)方法。這種設計的好處是有利於擴展,能夠後來方便的添加新的Event類型,然而在子類的實現中,須要判斷不一樣的Event類型而再次擴展成 不一樣的處理方法,從這個角度上來講,它又不利於擴展。另外在Netty3的使用過程當中,因爲它不停的建立ChannelEvent類,於是會引發GC的不穩定。
A multi-method interface:這種設計是將不一樣的Event類型在 EventHandler中定義相應的方法。這種設計就是Netty4中使用的策略,其中一個目的是避免ChannelEvent建立引發的GC不穩定, 另一個好處是它能夠避免在EventHandler實現時判斷不一樣的Event類型而有不一樣的實現,然而這種設計會給擴展新的Event類型時帶來很是 大的麻煩,由於它須要該接口。

關於Netty4對Netty3的改進能夠參考這裏網絡

ChannelHandler with no event objectIn 3.x, every I/O operation created a  ChannelEvent object. For each read / write, it additionally created a new  ChannelBuffer. It simplified the internals of Netty quite a lot because it delegates resource management and buffer pooling to the JVM. However, it often was the root cause of GC pressure and uncertainty which are sometimes observed in a Netty-based application under high load.

4.0 removes event object creation almost completely by replacing the event objects with strongly typed method invocations. 3.x had catch-all event handler methods such as handleUpstream() and handleDownstream(), but this is not the case anymore. Every event type has its own handler method now:多線程

爲何使用Reactor模式

歸功與Netty和Java NIO對Reactor的宣傳,本文慕名而學習的Reactor模式,於是已經默認Reactor具備很是優秀的性能,然而慕名歸慕名,到這裏,我仍是要不得不問本身Reactor模式的好處在哪裏?即爲何要使用這個Reactor模式?在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中是這麼說的:併發

Reactor Pattern優勢

Separation of concerns: The Reactor pattern decouples application-independent demultiplexing and dispatching mechanisms from application-specific hook method functionality. The application-independent mechanisms become reusable components that know how to demultiplex events and dispatch the appropriate hook methods defined by Event Handlers. In contrast, the application-specific functionality in a hook method knows how to perform a particular type of service.app

Improve modularity, reusability, and configurability of event-driven applications: The pattern decouples application functionality into separate classes. For instance, there are two separate classes in the logging server: one for establishing connections and another for receiving and processing logging records. This decoupling enables the reuse of the connection establishment class for different types of connection-oriented services (such as file transfer, remote login, and video-on-demand). Therefore, modifying or extending the functionality of the logging server only affects the implementation of the logging handler class.

Improves application portability: The Initiation Dispatcher’s interface can be reused independently of the OS system calls that perform event demultiplexing. These system calls detect and report the occurrence of one or more events that may occur simultaneously on multiple sources of events. Common sources of events may in- clude I/O handles, timers, and synchronization objects. On UNIX platforms, the event demultiplexing system calls are called select and poll [1]. In the Win32 API [16], the WaitForMultipleObjects system call performs event demultiplexing.

Provides coarse-grained concurrency control: The Reactor pattern serializes the invocation of event handlers at the level of event demultiplexing and dispatching within a process or thread. Serialization at the Initiation Dispatcher level often eliminates the need for more complicated synchronization or locking within an application process.

這些貌似是不少模式的共性:解耦、提高複用性、模塊化、可移植性、事件驅動、細力度的併發控制等,於是並不能很好的說明什麼,特別是它鼓吹的對性能的提高,這裏並無體現出來。固然在這篇文章的開頭有描述過另外一種直觀的實現:Thread-Per-Connection,即傳統的實現,提到了這個傳統實現的如下問題:

Thread Per Connection缺點

Efficiency: Threading may lead to poor performance due to context switching, synchronization, and data movement [2];

Programming simplicity: Threading may require complex concurrency control schemes;

Portability: Threading is not available on all OS platforms.

對於性能,它其實就是第一點關於Efficiency的描述,即線程的切換、同步、數據的移動會引發性能問題。也就是說從性能的角度上,它最大的提高就是減小了性能的使用,即不須要每一個Client對應一個線程。個人理解,其餘業務邏輯處理不少時候也會用到相同的線程,IO讀寫操做相對CPU的操做仍是要慢不少,即便Reactor機制中每次讀寫已經能保證非阻塞讀寫,這裏能夠減小一些線程的使用,可是這減小的線程使用對性能有那麼大的影響嗎?答案貌似是確定的,這篇論文(SEDA: Staged Event-Driven Architecture - An Architecture for Well-Conditioned, Scalable Internet Service)對隨着線程的增加帶來性能下降作了一個統計:

在這個統計中,每一個線程從磁盤中讀8KB數據,每一個線程讀同一個文件,於是數據自己是緩存在操做系統內部的,即減小IO的影響;全部線程是事先分配的,不會有線程啓動的影響;全部任務在測試內部產生,於是不會有網絡的影響。該統計數據運行環境:Linux 2.2.14,2GB內存,4-way 500MHz Pentium III。從圖中能夠看出,隨着線程的增加,吞吐量在線程數爲8個左右的時候開始線性降低,而且到64個之後而迅速降低,其相應事件也在線程達到256個後指數上升。即1+1<2,由於線程切換、同步、數據移動會有性能損失,線程數增長到必定數量時,這種性能影響效果會更加明顯。

對於這點,還能夠參考C10K Problem,用以描述同時有10K個Client發起鏈接的問題,到2010年的時候已經出現10M Problem了。

固然也有人說:Threads are expensive are no longer valid.在不久的未來可能又會發生不一樣的變化,或者這個變化正在、已經發生着?沒有作過比較仔細的測試,於是不敢隨便斷言什麼,然而本人觀點,即便線程變的影響並無之前那麼大,使用Reactor模式,甚至時SEDA模式來減小線程的使用,再加上其餘解耦、模塊化、提高複用性等優勢,仍是值得使用的。

Reactor模式的缺點

Reactor模式的缺點貌似也是顯而易見的:
1. 相比傳統的簡單模型,Reactor增長了必定的複雜性,於是有必定的門檻,而且不易於調試。
2. Reactor模式須要底層的Synchronous Event Demultiplexer支持,好比Java中的Selector支持,操做系統的select系統調用支持,若是要本身實現Synchronous Event Demultiplexer可能不會有那麼高效。
3. Reactor模式在IO讀寫數據時仍是在同一個線程中實現的,即便使用多個Reactor機制的狀況下,那些共享一個Reactor的Channel若是出現一個長時間的數據讀寫,會影響這個Reactor中其餘Channel的相應時間,好比在大文件傳輸時,IO操做就會影響其餘Client的相應時間,於是對這種操做,使用傳統的Thread-Per-Connection或許是一個更好的選擇,或則此時使用Proactor模式。

參考

Reactor Pattern WikiPedia
Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events
Scalable IO In Java
C10K Problem WikiPedia

相關文章
相關標籤/搜索