第
8
章
前攝器(
Proactor
):用于為異步事件多路分離和分派處理器的對象行為模式
Irfan Pyarali ? Tim Harrison?? Douglas C. Schmidt?? Thomas D. Jordan
現代操作系統為開發并發應用提供了多種機制。同步多線程是一種流行的機制,用于開發同時執行多個操作的應用。但是,線程常常有很高的性能開銷,并且需要對同步模式和原理有深入的了解。因此,有越來越多的操作系統支持異步機制,在減少多線程的大量開銷和復雜性的同時,提供了并發的好處。
本論文中介紹的前攝器(Proactor
)模式描述怎樣構造應用和系統,以有效地利用操作系統支持的異步機制。當應用調用異步操作時,OS
代表應用執行此操作。這使得應用可以讓多個操作同時運行,而又不需要應用擁有相應數目的線程。因此,通過使用更少的線程和有效利用OS
對異步操作的支持,前攝器模式簡化了并發編程,并改善了性能。
前攝器模式支持多個事件處理器的多路分離和分派,這些處理器由異步事件的完成來觸發。通過集成完成事件(completion event)的多路分離和相應的事件處理器的分派,該模式簡化了異步應用的開發。
這一部分提供使用前攝器模式的上下文和動機。
前攝器模式應該被用于應用需要并發執行操作的性能好處、又不想受到同步多線程或反應式編程的約束時。為說明這些好處,設想一個需要并發執行多個操作的網絡應用。例如,一個高性能Web服務器必須并發處理發送自多個客戶的HTTP請求[1, 2]。圖8-1 顯示了Web瀏覽器和Web服務器之間的典型交互。當用戶指示瀏覽器打開一個URL時,瀏覽器發送一個HTTP GET請求給Web服務器。收到請求,服務器就解析并校驗請求,并將指定的文件發回給瀏覽器。
圖8-1 典型的Web服務器通信軟件體系結構
開發高性能Web服務器要求消除以下壓力:
-
并發性:服務器必須同時執行多個客戶請求;
-
效率:服務器必須最小化響應延遲、最大化吞吐量,并避免不必要地使用CPU;
-
編程簡單性:服務器的設計應該簡化高效的并發策略的使用;
-
可適配性:應該使繼承新的或改進的傳輸協議(比如HTTP 1.1[3])所帶來的維護代價最小化。
Web服務器可以使用若干并發策略來實現,包括多個同步線程、反應式同步事件分派和前攝式異步事件分派。下面,我們檢查傳統方法的缺點,并解釋前攝器模式是怎樣提供一種強大的技術,為高性能并發應用而支持高效、靈活的異步事件分派策略的。
同步的多線程和反應式編程是實現并發的常用方法。這一部分描述這些編程模型的缺點。
或許最為直觀的實現并發Web服務器的途徑是使用同步的多線程。在此模型中,多個服務器線程同時處理來自多個客戶的HTTP GET請求。每個線程同步地執行連接建立、HTTP請求讀取、請求解析和文件傳輸操作。作為結果,每個操作都阻塞直到完成。
同步線程的主要優點是應用代碼的簡化。特別是,Web服務器為服務客戶A的請求所執行的操作在很大程度上獨立于為服務客戶B的請求所需的操作。因而,很容易在分離的線程中對不同的請求進行服務,因為在線程之間共享的狀態數量很少;這也最小化了對同步的需要。而且,在分離的線程中執行應用邏輯也使得開發者可以使用直觀的順序命令和阻塞操作。
圖8-2 多線程Web服務器體系結構
圖8-2顯示使用同步線程來設計的Web服務器怎樣并發地處理多個客戶請求。該圖顯示的Sync Acceptor對象封裝服務器端用于同步接受網絡連接的機制。使用“Thread Per Connection”并發模型,各個線程為服務HTTP GET請求所執行的一系列步驟可被總結如下:
- 每個線程同步地阻塞在accept socket調用中,等待客戶連接請求;
- 客戶連接到服務器,連接被接受;
- 新客戶的HTTP請求被同步地從網絡連接中讀取;
- 請求被解析;
- 所請求的文件被同步地讀取;
- 文件被同步地發送給客戶。
附錄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服務器
- Web服務器將Acceptor登記到Initiation Dispatcher,以接受新連接;
- Web服務器調用Initiation Dispatcher的事件循環;
- 客戶連接到Web服務器;
- Initiation Dispatcher將新連接請求通知Acceptor,后者接受新連接;
- Acceptor創建HTTP Handler,以服務新客戶;
- HTTP Handler將連接登記到Initiation Dispatcher,以讀取客戶請求數據(就是說,在連接變得“讀就緒”時);
- HTTP Handler服務來自新客戶的請求。
圖8-4顯示反應式Web服務器為服務HTTP GET請求所采取的一系列步驟。該過程描述如下:
- 客戶發送HTTP GET請求;
- 當客戶請求數據到達服務器時,Initiation Dispatcher通知HTTP Handler;
- 請求以非阻塞方式被讀取,于是如果操作會導致調用線程阻塞,讀操作就返回EWOULDBLOCK(步驟2和3將重復直到請求被完全讀取);
- HTTP Handler解析HTTP請求;
- 所請求的文件從文件系統中被同步讀取;
- 為發送文件數據(就是說,當連接變得“寫就緒”時),HTTP Handler將連接登記到Initiation Dispatcher;
- 當TCP連接變得寫就緒時,Initiation Dispatcher通知HTTP Handler;
- HTTP Handler以非阻塞方式將所請求文件發送給客戶,于是如果操作會導致調用線程阻塞,寫操作就返回EWOULDBLOCK(步驟7和8將重復直到數據被完全遞送)。
附錄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服務器時所采取的一系列步驟。
- Web服務器指示Acceptor發起異步接受;
- 接受器通過OS發起異步接受,將其自身作為Completion Handler和Completion Dispatcher的引用傳遞;并將用于在異步接受完成時通知Acceptor;
- Web服務器調用Completion Dispatcher的事件循環;
- 客戶連接到Web服務器;
- 當異步接受操作完成時,操作系統通知Completion Dispatcher;
- Completion Dispatcher通知接受器;
- Acceptor創建HTTP Handler;
- HTTP Handler發起異步操作,以讀取來自客戶的請求數據,并將其自身作為Completion Handler和Completion Dispatcher的引用傳遞;并將用于在異步讀取完成時通知HTTP Handler。
圖8-6 顯示前攝式Web服務器為服務HTTP GET請求所采取的步驟。這些步驟解釋如下:
- 客戶發送HTTP GET請求;
- 讀取操作完成,操作系統通知Completion Dispatcher;
- Completion Dispatcher通知HTTP Handler(步驟2和3將重復直到整個請求被接收);
- HTTP Handler解析請求;
- HTTP Handler同步地讀取所請求的文件;
- HTTP Handler發起異步操作,以把文件數據寫到客戶連接,并將其自身作為Completion Handler和Completion Dispatcher的引用傳遞;并將用于在異步寫入完成時通知HTTP Handler。
- 當寫操作完成時,操作系統通知Completion Dispatcher;
- 隨后Completion Dispatcher通知Completion Handler(步驟6-8將重復直到文件被完全遞送)。
8.8中有一個將前攝式事件分派模型應用于Web服務器的C++代碼例子。
使用前攝器模式的主要優點是可以啟動多個并發操作,并可并行運行,而不要求應用必須擁有多個線程。操作被應用異步地啟動,它們在OS的I/O子系統中運行直到完成。發起操作的線程現在可以服務另外的請求了。
例如,在上面的例子中,Completion Dispatcher可以是單線程的。當HTTP請求到達時,單個Completion Dispatcher線程解析請求,讀取文件,并發送響應給客戶。因為響應是被異步發送的,多個響應就有可能同時被發送。而且,同步的文件讀取可以被異步的文件讀取取代,以進一步增加并發的潛力。如果文件讀取是被異步完成的,HTTP Handler所執行的唯一的同步操作就只剩下了HTTP協議請求解析。
前攝式模型的主要缺點是編程邏輯至少和反應式模型一樣復雜。而且,前攝器模式可能會難以調試,因為異步操作常常有著不可預測和不可重復的執行序列,這就使分析和調試復雜化了。8.7描述怎樣應用其他模式(比如異步完成令牌[8])來簡化異步應用編程模型。
當具有以下一項或多項條件時使用前攝器模式:
- 應用需要執行一個或多個不阻塞調用線程的異步操作;
- 當異步操作完成時應用必須被通知;
- 應用需要獨立于它的I/O模型改變它的并發策略;
- 通過使依賴于應用的邏輯與應用無關的底層構造去耦合,應用將從中獲益;
- 當使用多線程方法或反應式分派方法時,應用的執行將很低效,或是不能滿足性能需求。
在圖8-7中使用OMT表示法演示了前攝器模式的結構。
前攝器模式中的關鍵參與者包括:
前攝發起器(Proactive Initiator。Web服務器應用的主線程):
- Proactive Initiator是應用中任何發起Asynchronous Operation(異步操作)的實體。它將Completion Handler和Completion Dispatcher登記到Asynchronous Operation Processor(異步操作處理器),此處理器在操作完成時通知前攝發起器。
完成處理器(Completion Handler。Acceptor和HTTP Handler):
- 前攝器模式將應用所實現的Completion Handler接口用于Asynchronous Operation完成通知。
異步操作(Asynchronous Operation。Async_Read、Async_Write和Async_Accept方法):
- Asynchronous Operation被用于代表應用執行請求(比如I/O和定時器操作)。當應用調用Asynchronous Operation時,操作的執行沒有借用應用的線程控制。因此,從應用的角度來看,操作是被異步地執行的。當Asynchronous Operation完成時,Asynchronous Operation Processor將應用通知委托給Completion Dispatcher。
異步操作處理器(Asynchronous Operation Processor。操作系統):
- Asynchronous Operation是由Asynchronous Operation Processor來運行直至完成的。該組件通常由OS實現。
完成分派器(Completion Dispatcher。Notification Queue):
- Completion Dispatcher負責在Asynchronous Operation完成時回調應用的Completion Handler。當Asynchronous Operation Processor完成異步發起的操作時,Completion Dispatcher代表應用執行應用回調。
圖8-7 前攝器模式中的參與者
有若干良好定義的步驟被用于所有Asynchronous Operation。在高水平的抽象上,應用異步地發起操作,并在操作完成時被通知。圖8-8顯示在模式參與者之間必定發生的下列交互:
-
前攝發起器發起操作:為執行異步操作,應用在Asynchronous Operation Processor上發起操作。例如,Web服務器可能要求OS在網絡上使用特定的socket連接傳輸文件。要請求這樣的操作,Web服務器必須指定要使用哪一個文件和網絡連接。而且,Web服務器必須指定(1)當操作完成時通知哪一個Completion Handler,以及(2)一旦文件被傳輸,哪一個Completion Dispatcher應該執行回調。
-
異步操作處理器執行操作:當應用在Asynchronous Operation Processor上調用操作時,它相對于其他應用操作異步地運行這些操作。現代操作系統(比如Solaris和Windows NT)在內核中提供異步的I/O子系統。
-
異步操作處理器通知完成分派器:當操作完成時,Asynchronous Operation Processor取得在操作被發起時指定的Completion Handler和Completion Dispatcher。隨后Asynchronous Operation Processor將Asynchronous Operation的結果和Completion Handler傳遞給Completion Dispatcher,以用于回調。例如,如果文件已被異步傳輸,Asynchronous Operation Processor可以報告完成狀態(比如成功或失敗),以及寫入網絡連接的字節數。
-
完成分派器通知應用:Completion Dispatcher在Completion Handler上調用完成掛鉤,將由應用指定的任何完成數據傳遞給它。例如,如果異步讀取完成,通常一個指向新到達數據的指針將會被傳遞給Completion Handler。
圖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,有三個主要步驟:
-
操作調用:因為操作將在與進行調用的應用線程不同的線程控制中執行,必定會發生某種類型的線程同步。一種方法是為每個操作派生一個線程。更為常用的方法是為Asynchronous Operation Processor而管理一個專用線程池。該方法可能需要應用線程在繼續進行其他應用計算之前將操作請求排隊。
-
操作執行:既然操作將在專用線程中執行,所以它可以執行“阻塞”操作,而不會直接阻礙應用的進展。例如,在提供異步I/O讀取機制時,專用線程可以在從socket或文件句柄中讀時阻塞。
-
操作完成:當操作完成時,應用必須被通知到。特別是,專用線程必須將應用特有的通知委托給Completion Dispatcher。這要求在線程間進行另外的同步。
當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之間的知識依賴。這有兩個好處:
- 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不是真正異步的)。
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 releas
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_;
};
本文轉載至ACE開發者網站 作者:Irfan Pyarali Tim Harrison
posted on 2007-02-27 21:17
walkspeed 閱讀(3283)
評論(0) 編輯 收藏 引用 所屬分類:
ACE Farmeworks