第8章 前攝器(Proactor):用於爲異步事件多路分離和分派處理器的對象行爲模式html
Irfan Pyarali Tim Harrison Douglas C. Schmidt Thomas D. Jordanreact
現代操做系統爲開發併發應用提供了多種機制。同步多線程是一種流行的機制,用於開發同時執行多個操做的應用。可是,線程經常有很高的性能開銷,而且須要對同步模式和原理有深刻的瞭解。所以,有愈來愈多的操做系統支持異步機制,在減小多線程的大量開銷和複雜性的同時,提供了併發的好處。編程
本論文中介紹的前攝器(Proactor)模式描述怎樣構造應用和系統,以有效地利用操做系統支持的異步機制。當應用調用異步操做時,OS表明應用執行此操做。這使得應用可讓多個操做同時運行,而又不須要應用擁有相應數目的線程。所以,經過使用更少的線程和有效利用OS對異步操做的支持,前攝器模式簡化了併發編程,並改善了性能。瀏覽器
前攝器模式支持多個事件處理器的多路分離和分派,這些處理器由異步事件的完成來觸發。經過集成完成事件(completion event)的多路分離和相應的事件處理器的分派,該模式簡化了異步應用的開發。安全
這一部分提供使用前攝器模式的上下文和動機。網絡
前攝器模式應該被用於應用須要併發執行操做的性能好處、又不想受到同步多線程或反應式編程的約束時。爲說明這些好處,設想一個須要併發執行多個操做的網絡應用。例如,一個高性能Web服務器必須併發處理髮送自多個客戶的HTTP請求[1, 2]。圖8-1 顯示了Web瀏覽器和Web服務器之間的典型交互。當用戶指示瀏覽器打開一個URL時,瀏覽器發送一個HTTP GET請求給Web服務器。收到請求,服務器就解析並校驗請求,並將指定的文件發回給瀏覽器。
圖8-1 典型的Web服務器通訊軟件體系結構
開發高性能Web服務器要求消除如下壓力:
Web服務器可使用若干併發策略來實現,包括多個同步線程、反應式同步事件分派和前攝式異步事件分派。下面,咱們檢查傳統方法的缺點,並解釋前攝器模式是怎樣提供一種強大的技術,爲高性能併發應用而支持高效、靈活的異步事件分派策略的。
同步的多線程和反應式編程是實現併發的經常使用方法。這一部分描述這些編程模型的缺點。
或許最爲直觀的實現併發Web服務器的途徑是使用同步的多線程。在此模型中,多個服務器線程同時處理來自多個客戶的HTTP GET請求。每一個線程同步地執行鏈接創建、HTTP請求讀取、請求解析和文件傳輸操做。做爲結果,每一個操做都阻塞直到完成。
同步線程的主要優勢是應用代碼的簡化。特別是,Web服務器爲服務客戶A的請求所執行的操做在很大程度上獨立於爲服務客戶B的請求所需的操做。於是,很容易在分離的線程中對不一樣的請求進行服務,由於在線程之間共享的狀態數量不多;這也最小化了對同步的須要。並且,在分離的線程中執行應用邏輯也使得開發者可使用直觀的順序命令和阻塞操做。
圖8-2 多線程Web服務器體系結構
圖8-2顯示使用同步線程來設計的Web服務器怎樣併發地處理多個客戶請求。該圖顯示的Sync Acceptor對象封裝服務器端用於同步接受網絡鏈接的機制。使用「Thread Per Connection」併發模型,各個線程爲服務HTTP GET請求所執行的一系列步驟可被總結以下:
附錄A.1中有一個將同步線程模型應用於Web服務器的C++代碼例子。
如上所述,每一個併發地鏈接的客戶由一個專用的服務器線程服務。在繼續爲其餘HTTP請求服務以前,該線程同步地完成一個被請求的操做。所以,要在服務多個客戶時執行同步I/O,Web服務器必須派生多個線程。儘管這種同步線程模式是直觀的,且可以相對高效地映射到多CPU平臺上,它仍是有如下缺點:
線程策略與併發策略被緊耦合:這種體系結構要求每一個相連客戶都有一個專用的線程。經過針對可用資源(好比使用線程池來對應CPU的數目)、而不是正被併發服務的客戶的數目來調整其線程策略,可能會更好地優化一個併發應用;
更大的同步複雜性:線程可能會增長序列化對服務器的共享資源(好比緩存文件和Web頁面點擊日誌)的訪問所必需的同步機制的複雜性;
更多的性能開銷:因爲上下文切換、同步和CPU間的數據移動[4],線程的執行可能很低效;
不可移植性:線程有可能在有些平臺上不可用。並且,根據對佔先式和非佔先式線程的支持,OS平臺之間的差別很是大。於是,很難構建可以跨平臺統一運做的多線程服務器。
做爲這些缺點的結果,多線程經常不是開發併發Web服務器的最爲高效的、也不是最不復雜的解決方案。
另外一種實現同步Web服務器的經常使用方法是使用反應式事件分派模型。反應堆(Reactor)模式描述應用怎樣將Event Handler登記到Initiation Dispatcher。Initiation Dispatcher通知Event Handler什麼時候能發起一項操做而不阻塞。
單線程併發Web服務器可使用反應式事件分派模型,它在一個事件循環中等待Reactor通知它發起適當的操做。Web服務器中反應式操做的一個例子是Acceptor(接受器)[6]到Initiation Dispatcher的登記。當數據在網絡鏈接上到達時,分派器回調Acceptor,後者接受網絡鏈接,並建立HTTP Handler。因而這個HTTP Handler就登記到Reactor,以在Web服務器的單線程控制中處理在那個鏈接上到來的URL請求。
圖8-3和圖8-4顯示使用反應式事件分派設計的Web服務器怎樣處理多個客戶。圖8-3顯示當客戶鏈接到Web服務器時所採起的步驟。圖8-4顯示Web服務器怎樣處理客戶請求。圖8-3的一系列步驟可被總結以下:
圖8-3 客戶鏈接到反應式Web服務器
圖8-4 客戶發送HTTP請求到反應式Web服務器
圖8-4顯示反應式Web服務器爲服務HTTP GET請求所採起的一系列步驟。該過程描述以下:
附錄A.2中有一個將反應式事件分派模型應用於Web服務器的C++代碼例子。
由於Initiation Dispatcher運行在單線程中,網絡I/O操做以非阻塞方式運行在Reactor的控制之下。若是當前操做的進度中止了,操做就被轉手給Initiation Dispatcher,由它監控系統操做的狀態。當操做能夠再度前進時,適當的Event Handler會被通知。
反應式模式的主要優勢是可移植性,粗粒度併發控制帶來的低開銷(就是說,單線程不須要同步或上下文切換),以及經過使應用邏輯與分派機制去耦合所得到的模塊性。可是,該方法有如下缺點:
複雜的編程:如從前面的列表所看到的,程序員必須編寫複雜的邏輯,以保證服務器不會在服務一個特定客戶時阻塞。
缺少多線程的OS支持:大多數操做系統經過select系統調用[7]來實現反應式分派模型。可是,select不容許多於一個的線程在同一個描述符集上等待。這使得反應式模型不適用於高性能應用,由於它沒有有效地利用硬件的並行性。
可運行任務的調度:在支持佔先式線程的同步多線程體系結構中,將可運行線程調度並時分(time-slice)到可用CPU上是操做系統的責任。這樣的調度支持在反應式體系結構中不可用,由於在應用中只有一個線程。所以,系統的開發者必須當心地在全部鏈接到Web服務器的客戶之間將線程分時。這隻能經過執行短持續時間、非阻塞的操做來完成。
做爲這些缺點的結果,當硬件並行可用時,反應式事件分派不是最爲高效的模型。因爲須要避免使用阻塞I/O,該模式還有着相對較高的編程複雜度。
當OS平臺支持異步操做時,一種高效而方便的實現高性能Web服務器的方法是使用前攝式事件分派。使用前攝式事件分派模型設計的Web服務器經過一或多個線程控制來處理異步操做的完成。這樣,經過集成完成事件多路分離(completion event demultiplexing)和事件處理器分派,前攝器模式簡化了異步的Web服務器。
異步的Web服務器將這樣來利用前攝器模式:首先讓Web服務器向OS發出異步操做,並將回調方法登記到Completion Dispatcher(完成分派器),後者將在操做完成時通知Web服務器。因而OS表明Web服務器執行操做,並隨即在一個周知的地方將結果排隊。Completion Dispatcher負責使完成通知出隊,並執行適當的、含有應用特有的Web服務器代碼的回調。
圖8-5 客戶鏈接到基於前攝器的Web服務器
圖8-6 客戶發送請求給基於前攝器的Web服務器
圖8-5和圖8-6顯示使用前攝式事件分派設計的Web服務器怎樣在一或多個線程中併發地處理多個客戶。圖8-5顯示當客戶鏈接到Web服務器時所採起的一系列步驟。
圖8-6 顯示前攝式Web服務器爲服務HTTP GET請求所採起的步驟。這些步驟解釋以下:
8.8中有一個將前攝式事件分派模型應用於Web服務器的C++代碼例子。
使用前攝器模式的主要優勢是能夠啓動多個併發操做,並可並行運行,而不要求應用必須擁有多個線程。操做被應用異步地啓動,它們在OS的I/O子系統中運行直到完成。發起操做的線程如今能夠服務另外的請求了。
例如,在上面的例子中,Completion Dispatcher能夠是單線程的。當HTTP請求到達時,單個Completion Dispatcher線程解析請求,讀取文件,併發送響應給客戶。由於響應是被異步發送的,多個響應就有可能同時被髮送。並且,同步的文件讀取能夠被異步的文件讀取取代,以進一步增長併發的潛力。若是文件讀取是被異步完成的,HTTP Handler所執行的惟一的同步操做就只剩下了HTTP協議請求解析。
前攝式模型的主要缺點是編程邏輯至少和反應式模型同樣複雜。並且,前攝器模式可能會難以調試,由於異步操做經常有着不可預測和不可重複的執行序列,這就使分析和調試複雜化了。8.7描述怎樣應用其餘模式(好比異步完成令牌[8])來簡化異步應用編程模型。
當具備如下一項或多項條件時使用前攝器模式:
在圖8-7中使用OMT表示法演示了前攝器模式的結構。
前攝器模式中的關鍵參與者包括:
前攝發起器(Proactive Initiator。Web服務器應用的主線程):
完成處理器(Completion Handler。Acceptor和HTTP Handler):
異步操做(Asynchronous Operation。Async_Read、Async_Write和Async_Accept方法):
異步操做處理器(Asynchronous Operation Processor。操做系統):
完成分派器(Completion Dispatcher。Notification Queue):
圖8-7 前攝器模式中的參與者
有若干良好定義的步驟被用於全部Asynchronous Operation。在高水平的抽象上,應用異步地發起操做,並在操做完成時被通知。圖8-8顯示在模式參與者之間一定發生的下列交互:
圖8-8 前攝器模式的交互圖
這一部分詳述使用前攝器模式的效果。
前攝器模式提供如下好處:
加強事務分離:前攝器模式使應用無關的異步機制與應用特有的功能去耦合。應用無關的機制成爲可複用組件,知道怎樣多路分離與Asynchronous Operation相關聯的完成事件,並分派適當的由Completion Handler定義的回調方法。一樣地,應用特有的功能知道怎樣執行特定類型的服務(好比HTTP處理)。
改善應用邏輯可移植性:經過容許接口獨立於執行事件多路分離的底層OS調用而複用,它改善了應用的可移植性。這些系統調用檢測並報告可能同時發生在多個事件源之上的事件。事件源能夠是I/O端口、定時器、同步對象、信號,等等。在實時POSIX平臺上,異步I/O函數由aio API族[9]提供。在Windows NT中,I/O完成端口和重疊式(overlapped)I/O被用於實現異步I/O[10]。
完成分派器封裝了併發機制:使Completion Dispatcher與Asynchronous Operation Processor去耦合的一個好處是應用能夠經過多種併發策略來配置Completion Dispatcher,而不會影響其餘參與者。如8.7所討論的,Completion Dispatcher可被配置使用包括單線程和線程池方案在內的若干併發策略。
線程策略被與併發策略去耦合:由於Asynchronous Operation Processor表明Proactive Initiator完成可能長時間運行的操做,應用不會被迫派生線程來增長併發。這使得應用能夠獨立於它的線程策略改變它的併發策略。例如,Web服務器可能只想每一個CPU有一個線程,但又想同時服務更多數目的客戶。
提升性能:多線程操做系統執行上下文切換,以在多個線程控制中輪換。雖然執行一次上下文切換的時間保持至關的恆定,若是OS上下文要切換到空閒線程的話,在大量線程間輪換的總時間能夠顯著地下降應用性能。例如,線程能夠輪詢OS以查看完成狀態,而這是低效率的。經過只激活那些有事件要處理的合理的線程控制,前攝器模式可以避免上下文切換的代價。例如,若是沒有待處理的GET請求,Web服務器不須要啓用HTTP Handler。
應用同步的簡化:只要Completion Handler不派生另外的線程控制,能夠不考慮、或只考慮少量同步問題而編寫應用邏輯。Completion Handler可被編寫爲就好像它們存在於一個傳統的單線程環境中同樣。例如,Web服務器的HTTP GET處理器能夠經過Async Read操做(好比Windows NT TransmitFile函數[1])來訪問磁盤。
前攝器模式有如下缺點:
難以調試:之前攝器模式編寫的應用可能難以調試,由於反向的控制流在構架基礎結構和應用特有的處理器上的回調方法之間來回振盪。這增長了在調試器中對構架的運行時行爲的「單步跟蹤」的困難度,由於應用開發者可能不瞭解或不能得到構架的代碼。這與試圖調試使用LEX和YACC編寫的編譯器的詞法分析器和解析器時所遇到的問題是相似的。在這些應用中,當線程控制是在用戶定義的動做例程中時,調試是至關直接的。可是一旦線程控制返回到所生成的有限肯定自動機(Deterministic Finite Automate,DFA)骨架時,就很難跟住程序邏輯了。
調度和控制未完成操做:Proactive Initiator可能沒有對Asynchronous Operation的執行順序的控制。所以,Asynchronous Operation Processor必須被當心設計,以支持Asynchronous Operation的優先級和取消處理。
前攝器模式能夠經過許多方式實現。這一部分討論實現前攝器模式所涉及的步驟。
實現前攝器模式的第一步是構建Asynchronous Operation Processor。該組件負責表明應用異步地執行操做。所以,它的兩項主要責任是輸出Asynchronous Operation API和實現Asynchronous Operation Engine以完成工做。
Asynchronous Operation Processor必須提供API、容許應用請求Asynchronous Operation。在設計這些API時有若干壓力須要考慮:
可移植性:此API不該約束應用或它的Proactive Initiator使用特定的平臺。
靈活性:經常,異步API能夠爲許多類型的操做共享。例如,異步I/O操做經常被用於在多種介質(好比網絡和文件)上執行I/O。設計支持這樣的複用的API多是有益的。
回調:當操做被調用時,Proactive Initiator必須登記回調。實現回調的一種經常使用方法是讓調用對象(客戶)輸出接口、讓調用者知道(服務器)。所以,Proactive Initiator必須通知Asynchronous Operation Processor,當操做完成時,哪個Completion Handler應被回調。
完成分派器:由於應用可使用多個Completion Dispatcher,Proactive Initiator還必須指示由哪個Completion Dispatcher來執行回調。
給定全部這些問題,考慮下面的用於異步讀寫的API。Asynch_Stream類是用於發起異步讀寫的工廠。一旦構造,可使用此類來啓動多個異步讀寫。當異步讀取完成時,Asynch_Stream::Read_Result將經過Completion_Handler上的handler_read回調方法被回傳給handler。相似地,當異步寫入完成時,Asynch_Stream::Write_Result將經過Completion_Handler上的handler_write回調方法被回傳給handler。
class Asynch_Stream
// = TITLE
// A Factory for initiating reads
// and writes asynchronously.
{
// Initializes the factory with information
// which will be used with each asynchronous
// call. <handler> is notified when the
// operation completes. The asynchronous
// operations are performed on the <handle>
// and the results of the operations are
// sent to the <Completion_Dispatcher>.
Asynch_Stream (Completion_Handler &handler,
HANDLE handle,
Completion_Dispatcher *);
// This starts off an asynchronous read.
// Upto <bytes_to_read> will be read and
// stored in the <message_block>.
int read (Message_Block &message_block,
u_long bytes_to_read,
const void *act = 0);
// This starts off an asynchronous write.
// Upto <bytes_to_write> will be written
// from the <message_block>.
int write (Message_Block &message_block,
u_long bytes_to_write,
const void *act = 0);
...
};
Asynchronous Operation Processor必須含有異步執行操做的機制。換句話說,當應用線程調用Asynchronous Operation時,必須不借用應用的線程控制而執行此操做。幸虧,現代操做系統提供了用於Asynchronous Operation的機制(例如,POSIX 異步I/O和WinNT重疊式I/O)。在這樣的狀況下,實現模式的這一部分只須要簡單地將平臺API映射到上面描述的Asynchronous Operation API。
若是OS平臺不提供對Asynchronous Operation的支持,有若干實現技術可用於構建Asynchronous Operation Engine。或許最爲直觀的解決方案是使用專用線程來爲應用執行Asynchronous Operation。要實現線程化的Asynchronous Operation Engine,有三個主要步驟:
當Completion Dispatcher從Asynchronous Operation Processor接收到操做完成通知時,它會回調與應用對象相關聯的Completion Handler。實現Completion Dispatcher涉及兩個問題:(1)實現回調以及(2)定義用於執行回調的併發策略。
Completion Dispatcher必須實現一種機制,Completion Handler經過它被調用。這要求Proactive Initiator在發起操做時指定一個回調。下面是經常使用的回調可選方案:
回調類:Completion Handler輸出接口、讓Completion Dispatcher知道。當操做完成時,Completion Dispatcher回調此接口中的方法,並將已完成操做的有關信息傳遞給它(好比從網絡鏈接中讀取的字節數)。
函數指針:Completion Dispatcher經過回調函數指針來調用Completion Handler。該方法有效地打破了Completion Dispatcher和Completion Handler之間的知識依賴。這有兩個好處:
會合點:Proactive Initiator能夠設立事件對象或條件變量,用做Completion Dispatcher和Completion Handler之間的會合點。這在Completion Handler是Proactive Initiator時最爲常見。在Asynchronous Operation運行至完成的同時,Completion Handler處理其餘的活動。Completion Handler將在會合點週期性地檢查完成狀態。
當操做完成時,Asynchronous Operation Processor將會通知Completion Dispatcher。在這時,Completion Dispatcher能夠利用下面的併發策略中的一種來執行應用回調:
動態線程分派:Completion Dispatcher可爲每一個Completion Handler動態分配一個線程。動態線程分派可經過大多數多線程操做系統來實現。在有些平臺上,因爲建立和銷燬線程資源的開銷,這多是所列出的Completion Dispatcher實現技術中最爲低效的一種,
後反應式分派(Post-reactive dispatching):Completion Dispatcher能夠發信號給Proactive Initiation所設立的事件對象或條件變量。儘管輪詢和派生阻塞在事件對象上的子線程都是可選的方案,最爲高效的後反應式分派方法是將事件登記到Reactor。後反應式分派能夠經過POSIX實時環境中的aio_suspend和Win32環境中的WaitForMultipleObjects來實現。
Call-through分派:來自Asynchronous Operation Processor的線程控制可被Completion Dispatcher借用,以執行Completion Handler。這種「週期偷取」策略能夠經過減小空閒線程的影響範圍來提升性能。在一些老操做系統會將上下文切換到空閒線程、又只是從它們切換出去的狀況下,這種方法有着收回「失去的」時間的巨大潛力。
Call-through分派在Windows NT中可使用ReadFileEx和WriteFileEx Win32函數來實現。例如,線程控制可使用這些調用來等待信號量被置位。當它等待時,線程通知OS它進入了一種稱爲「可報警等待狀態」(alterable wait state)的特殊狀態。在這時,OS能夠佔有對等待中的線程控制的棧和相關資源的控制,以執行Completion Handler。
線程池分派:由Completion Dispatcher擁有的線程池可被用於Completion Handler的執行。在池中的每一個線程控制已被動態地分配到可用的CPU。線程池分派可經過Windows NT的I/O完成端口來實現。
在考慮上面描述的Completion Dispatcher技術的適用性時,考慮表8-1中所示的OS環境和物理硬件的可能組合:
線程模型 |
系統類型 |
|
單處理器 |
多處理器 |
|
單線程 |
A |
B |
多線程 |
C |
D |
表8-1 Completion Dispatcher併發策略
若是你的OS只支持同步I/O,那就參見反應堆模式[5]。可是,大多數現代操做系統都支持某種類型的異步I/O。
在表8-1的A和B組合中,假定你不等待任何信號量或互斥體,後反應方式的異步I/O極可能是最好的。不然,Call-through實現或許更能迴應你的問題。在C組合中,使用Call-through方法。在D組合中,使用線程池方法。在實踐中,系統化的經驗測量對於選擇最爲合適的可選方案來講是必需的。
Completion Handler的實現帶來如下考慮。
Completion Handler可能須要維護關於特定請求的狀態信息。例如,OS能夠通知Web服務器,只有一部分文件已被寫到網絡通訊端口。做爲結果,Completion Handler可能須要從新發出請求,直到文件被徹底寫出,或鏈接變得無效。所以,它必須知道原先指定的文件,還剩多少字節要寫,以及在前一個請求開始時文件指針的位置。
沒有隱含的限制來阻止Proactive Initiator將多個Asynchronous Operation請求分配給單個Completion Handler。所以,Completion Handler必須在完成通知鏈中一一「繫上」請求特有的狀態信息。爲完成此工做,Completion Handler能夠利用異步完成令牌(Asynchronous Completion Token)模式[8]。
與在任何多線程環境中同樣,使用前攝器模式的Completion Handler仍是要由它本身來確保對共享資源的訪問是線程安全的。可是,Completion Handler不能跨越多個完成通知持有共享資源。不然,就有發生「用餐哲學家問題」的危險[11]。
該問題在於一個合理的線程控制永久等待一個信號量被置位時所產生的死鎖。經過設想一個由一羣哲學家出席的宴會能夠演示這一問題。用餐者圍繞一個圓桌就座,在每一個哲學家之間只有一支筷子。當哲學家以爲飢餓時,他必須獲取在他左邊和在他右邊的筷子才能用餐。一旦哲學家得到一支筷子,不到吃飽他們就不會放下它。若是全部哲學家都拿起在他們右邊的筷子,就會發生死鎖,由於他們將永遠也不可能拿到左邊的筷子。
8.7.3.3 佔先式策略(Preemptive Policy)
Completion Dispatcher類型決定在執行時一個Completion Handler是否可佔先。當與動態線程和線程池分派器相連時,Completion Handler天然可佔先。可是,當與後反應式Completion Dispatcher相連時,Completion Handler並無對其餘Completion Handler的佔先權。當由Call-through分派器驅動時,Completion Handler相對於在可報警等待狀態的線程控制也沒有佔先權。
通常而言,處理器不該該執行持續時間長的同步操做,除非使用了多個完成線程,由於應用的整體響應性將會被顯著地下降。這樣的危險能夠經過加強的編程訓練來下降。例如,全部Completion Handler被要求用做Proactive Initiator,而不是去執行同步操做。
這一部分顯示怎樣使用前攝器模式來開發Web服務器。該例子基於ACE構架[4]中的前攝器實現。
當客戶鏈接到Web服務器時,HTTP_Handler的open方法被調用。因而服務器就經過在Asynchronous Operation完成時回調的對象(在此例中是this指針)、用於傳輸數據的網絡鏈接,以及一旦操做完成時使用的Completion Dispatcher(proactor_)來初始化異步I/O對象。隨後讀操做異步地啓動,而服務器返回事件循環。
當Async read操做完成時,分派器回調HTTP_Handler::handle_read_stream。若是有足夠的數據,客戶請求就被解析。若是整個客戶請求還未徹底到達,另外一個讀操做就會被異步地發起。
在對GET請求的響應中,服務器對所請求文件進行內存映射,並將文件數據異步地寫往客戶。當寫操做完成時,分派器回調HTTP_Handler::handle_write_stream,從而釋放動態分配的資源。
附錄中含有兩個其餘的代碼實例,使用同步的線程模型和同步的(非阻塞)反應式模型實現Web服務器。
class HTTP_Handler
: public Proactor::Event_Handler
// = TITLE
// Implements the HTTP protocol
// (asynchronous version).
//
// = PATTERN PARTICIPANTS
// Proactive Initiator = HTTP_Handler
// Asynch Op = Network I/O
// Asynch Op Processor = OS
// Completion Dispatcher = Proactor
// Completion Handler = HTPP_Handler
{
public:
void open (Socket_Stream *client)
{
// Initialize state for request
request_.state_ = INCOMPLETE;
// Store reference to client.
client_ = client;
// Initialize asynch read stream
stream_.open (*this, client_->handle (), proactor_);
// Start read asynchronously.
stream_.read (request_.buffer (),
request_.buffer_size ());
}
// This is called by the Proactor
// when the asynch read completes
void handle_read_stream(u_long bytes_transferred)
{
if (request_.enough_data(bytes_transferred))
parse_request ();
else
// Start reading asynchronously.
stream_.read (request_.buffer (),
request_.buffer_size ());
}
void parse_request (void)
{
// Switch on the HTTP command type.
switch (request_.command ())
{
// Client is requesting a file.
case HTTP_Request::GET:
// Memory map the requested file.
file_.map (request_.filename ());
// Start writing asynchronously.
stream_.write (file_.buffer (), file_.buffer_size ());
break;
// Client is storing a file
// at the server.
case HTTP_Request::PUT:
// ...
}
}
void handle_write_stream(u_long bytes_transferred)
{
if (file_.enough_data(bytes_transferred))
// Success....
else
// Start another asynchronous write
stream_.write (file_.buffer (), file_.buffer_size ());
}
private:
// Set at initialization.
Proactor *proactor_;
// Memory-mapped file_;
Mem_Map file_;
// Socket endpoint.
Socket_Stream *client_;
// HTTP Request holder
HTTP_Request request_;
// Used for Asynch I/O
Asynch_Stream stream_;
};
下面是一些被普遍記載的前攝器的使用:
Windows NT中的I/O完成端口:Windows NT操做系統實現了前攝器模式。Windows NT支持多種Asynchronous Operation,好比接受新網絡鏈接、讀寫文件和socket,以及經過網絡鏈接傳輸文件。操做系統就是Asynchronous Operation Processor。操做結果在I/O完成端口(它扮演Completion Dispatcher的角色)上排隊。
異步I/O操做的UNIX AIO族:在有些實時POSIX平臺上,前攝器模式是由aio API族[9]來實現的。這些OS特性很是相似於上面描述的Windows NT的特性。一個區別是UNIX信號可用於實現真正異步的Completion Dispatcher(Windows NT API不是真正異步的)。
ACE Proactor:ACE自適配通訊環境 [4]實現了前攝器組件,它封裝Windows NT上的I/O完成端口,以及POSIX平臺上的aio API。ACE前攝器抽象提供Windows NT所支持的標準C API的OO接口。這一實現的源碼可從ACE網站http://www.cs.wustl.edu/~schmidt/ACE.html獲取。
Windows NT中的異步過程調用(Asynchronous Procedure Call):有些系統(好比Windows NT)支持異步過程調用(APC)。APC是在特定線程的上下文中異步執行的函數。當APC被排隊到線程時,系統發出軟件中斷。下一次線程被調度時,它將運行該APC。操做系統所發出的APC被稱爲內核模式APC。應用所發出的APC被稱爲用戶模式APC。
圖8-9演示與前攝器相關的模式。
圖8-9 前攝器模式的相關模式
異步完成令牌(ACT)模式[8]一般與前攝器模式結合使用。當Asynchronous Operation完成時,應用可能須要比簡單的通知更多的信息來適當地處理事件。異步完成令牌模式容許應用將狀態高效地與Asynchronous Operation的完成相關聯。
前攝器模式還與觀察者(Observer)模式[12](在其中,當單個主題變更時,相關對象也會自動更新)有關。在前攝器模式中,當來自多個來源的事件發生時,處理器被自動地通知。通常而言,前攝器模式被用於異步地將多個輸入源多路分離給與它們相關聯的事件處理器,而觀察者一般僅與單個事件源相關聯。
前攝器模式可被認爲是同步反應堆模式[5]的一種異步的變體。反應堆模式負責多個事件處理器的多路分離和分派;它們在能夠同步地發起操做而不會阻塞時被觸發。相反,前攝器模式也支持多個事件處理器的多路分離和分派,但它們是被異步事件的完成觸發的。
主動對象(Active Object)模式[13]使方法執行與方法調用去耦合。前攝器模式也是相似的,由於Asynchronous Operation Processor表明應用的Proactive Initiator來執行操做。就是說,兩種模式均可用於實現Asynchronous Operation。前攝器模式經常用於替代主動對象模式,以使系統併發策略與線程模型去耦合。
前攝器可被實現爲單體(Singleton)[12]。這對於在異步應用中,將事件多路分離和完成分派集中到單一的地方來講是有用的。
責任鏈(Chain of Responsibility,COR)模式[12]使事件處理器與事件源去耦合。在Proactive Initiator與Completion Handler的隔離上,前攝器模式也是相似的。可是,在COR中,事件源預先不知道哪個處理器將被執行(若是有的話)。在前攝器中,Proactive Initiator徹底知道目標處理器。可是,經過創建一個Completion Handler(它是由外部工廠動態配置的責任鏈的入口),這兩種模式可被結合在一塊兒:。
前攝器模式包含了一種強大的設計範式,支持高性能併發應用的高效而靈活的事件分派策略。前攝器模式提供併發執行操做的性能助益,而又不強迫開發者使用同步多線程或反應式編程。
[1] J. Hu, I. Pyarali, and D. C. Schmidt, 「Measuring the Impact of Event Dispatching and Concurrency Models on Web Server Performance Over High-speed Networks,」 in Proceedings of the 2nd Global Internet Conference, IEEE, November 1997.
[2] J. Hu, I. Pyarali, and D. C. Schmidt, 「Applying the Proactor Pattern to High-Performance Web Servers,」 in Proceedings of the 10th International Conference on Parallel and Distributed Computing and Systems, IASTED, Oct. 1998.
[3] J. C. Mogul, 「The Case for Persistent-connection HTTP,」 in Proceedings of ACMSIGCOMM ’95 Conference in Computer Communication Review, (Boston, MA, USA), pp. 299–314, ACM Press, August 1995.
[4] D. C. Schmidt, 「ACE: an Object-Oriented Framework for Developing Distributed Applications,」 in Proceedings of the 6th USENIX C++ Technical Conference, (Cambridge, Massachusetts), USENIX Association, April 1994.
[5] D. C. Schmidt, 「Reactor: An Object Behavioral Pattern for Concurrent Event Demultiplexing and Event Handler Dispatching,」 in Pattern Languages of Program Design (J. O. Coplien and D. C. Schmidt, eds.), pp. 529–545, Reading, MA: Addison-Wesley, 1995.
[6] D. C. Schmidt, 「Acceptor and Connector: Design Patterns for Initializing Communication Services,」 in Pattern Languages of Program Design (R. Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997.
[7] M. K. McKusick, K. Bostic, M. J. Karels, and J. S. Quarterman, The Design and Implementation of the 4.4BSD Operating System. Addison Wesley, 1996.
[8] I. Pyarali, T. H. Harrison, and D. C. Schmidt, 「Asynchronous Completion Token: an Object Behavioral Pattern for Efficient Asynchronous Event Handling,」 in Pattern Languages of Program Design (R. Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997.
[9] 「Information Technology – Portable Operating System Interface (POSIX) – Part 1: System Application: Program Interface (API) [C Language],」 1995.
[10] Microsoft Developers Studio, Version 4.2 - Software Development Kit, 1996.
[11] E. W. Dijkstra, 「Hierarchical Ordering of Sequential Processes,」 Acta Informatica, vol. 1, no. 2, pp. 115–138, 1971.
[12] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.
[13] R. G. Lavender and D. C. Schmidt, 「Active Object: an Object Behavioral Pattern for Concurrent Programming,」 in Proceedings of the 2nd Annual Conference on the Pattern Languages of Programs, (Monticello, Illinois), pp. 1–7, September 1995.
本附錄概述用於開發前攝器模式的可選實現的代碼。下面,咱們檢查使用多線程的同步I/O和使用單線程的反應式I/O。
下面的代碼顯示怎樣使用線程池同步I/O來開發Web服務器。當客戶鏈接到服務器時,池中的一個線程接受鏈接,並調用HTTP_Handler中的open方法。隨後服務器同步地從網絡鏈接讀取請求。當讀操做完成時,客戶請求隨之被解析。在對GET請求的響應中,服務器對所請求文件進行內存映射,並將文件數據同步地寫往客戶。注意阻塞I/O是怎樣使Web服務器可以遵循2.2.1中所概述的步驟的。
class HTTP_Handler
// = TITLE
// Implements the HTTP protocol
// (synchronous threaded version).
//
// = DESCRIPTION
// This class is called by a
// thread in the Thread Pool.
{
public:
void open (Socket_Stream *client)
{
HTTP_Request request;
// Store reference to client.
client_ = client;
// Synchronously read the HTTP request
// from the network connection and
// parse it.
client_->recv (request);
parse_request (request);
}
void parse_request (HTTP_Request &request)
{
// Switch on the HTTP command type.
switch (request.command ())
{
// Client is requesting a file.
case HTTP_Request::GET:
// Memory map the requested file.
Mem_Map input_file;
input_file.map (request.filename());
// Synchronously send the file
// to the client. Block until the
// file is transferred.
client_->send (input_file.data (),
input_file.size ());
break;
// Client is storing a file at
// the server.
case HTTP_Request::PUT:
// ...
}
}
private:
// Socket endpoint.
Socket_Stream *client_;
// ...
};
下面的代碼顯示怎樣將反應堆模式用於開發Web服務器。當客戶鏈接到服務器時,HTTP_Handler::open方法被調用。服務器登記I/O句柄和在網絡句柄「讀就緒「時回調的對象(在此例中是this指針)。而後服務器返回事件循環。
當請求數據到達服務器時,reactor_回調HTTP_Handler::handle_input方法。客戶數據以非阻塞方式被讀取。若是有足夠的數據,客戶請求就被解析。若是整個客戶請求尚未到達,應用就返回反應堆事件循環。
在對GET請求的響應中,服務器對所請求的文件進行內存映射;並在反應堆上登記,以在網絡鏈接變爲「寫就緒」時被通知。當向鏈接寫入數據不會阻塞調用線程時,reactor_就回調HTTP_Handler::handler_output方法。當全部數據都已發送給客戶時,網絡鏈接被關閉。
class HTTP_Handler :
public Reactor::Event_Handler
// = TITLE
// Implements the HTTP protocol
// (synchronous reactive version).
//
// = DESCRIPTION
// The Event_Handler base class
// defines the hooks for
// handle_input()/handle_output().
//
// = PATTERN PARTICIPANTS
// Reactor = Reactor
// Event Handler = HTTP_Handler
{
public:
void open (Socket_Stream *client)
{
// Initialize state for request
request_.state_ = INCOMPLETE;
// Store reference to client.
client_ = client;
// Register with the reactor for reading.
reactor_->register_handler
(client_->handle (),
this,
Reactor::READ_MASK);
}
// This is called by the Reactor when
// we can read from the client handle.
void handle_input (void)
{
int result = 0;
// Non-blocking read from the network
// connection.
do
result = request_.recv (client_->handle ());
while (result != SOCKET_ERROR && request_.state_ == INCOMPLETE);
// No more progress possible,
// blocking will occur
if (request_.state_ == INCOMPLETE && errno == EWOULDBLOCK)
reactor_->register_handler
(client_->handle (),
this,
Reactor::READ_MASK);
else
// We now have the entire request
parse_request ();
}
void parse_request (void)
{
// Switch on the HTTP command type.
switch (request_.command ())
{
// Client is requesting a file.
case HTTP_Request::GET:
// Memory map the requested file.
file_.map (request_.filename ());
// Transfer the file using Reactive I/O.
handle_output ();
break;
// Client is storing a file at
// the server.
case HTTP_Request::PUT:
// ...
}
}
void handle_output (void)
{
// Asynchronously send the file
// to the client.
if (client_->send (file_.data (),
file_.size ())
== SOCKET_ERROR
&& errno == EWOULDBLOCK)
// Register with reactor...
else
// Close down and release resources.
handle_close ();
}
private:
// Set at initialization.
Reactor *reactor_;
// Memory-mapped file_;
Mem_Map file_;
// Socket endpoint.
Socket_Stream *client_;
// HTTP Request holder.
HTTP_Request request_;
};