[轉]兩種高性能I/O設計模式(Reactor/Proactor)的比較

原文地址:http://www.cppblog.com/pansunyou/archive/2011/01/26/io_design_patterns.htmlphp

綜述html

這篇文章探討並比較兩種用於TCP服務器的高性能設計模式. 除了介紹現有的解決方案, 還提出了一種更具伸縮性,只須要維護一份代碼而且跨平臺的解決方案(含代碼示例), 以及其在不一樣平臺上的微調. 此文還比較了java,c#,c++對各自現有以及提到的解決方案的實現性能.java

系統I/O 可分爲阻塞型, 非阻塞同步型以及非阻塞異步型[12]. 阻塞型I/O意味着控制權只到調用操做結束了纔會回到調用者手裏. 結果調用者被阻塞了, 這段時間了作不了任何其它事情. 更鬱悶的是,在等待IO結果的時間裏,調用者所在線程此時沒法騰出手來去響應其它的請求,這真是太浪費資源了。拿read()操做來講吧, 調用此函數的代碼會一直僵在此處直至它所讀的socket緩存中有數據到來.linux

相比之下,非阻塞同步是會當即返回控制權給調用者的。調用者不須要等等,它從調用的函數獲取兩種結果:要麼這次調用成功進行了;要麼系統返回錯誤標識告訴調用者當前資源不可用,你再等等或者再試度看吧。好比read()操做, 若是當前socket無數據可讀,則當即返回EWOULBLOCK/EAGAIN,告訴調用read()者"數據還沒準備好,你稍後再試".c++

在非阻塞異步調用中,稍有不一樣。調用函數在當即返回時,還告訴調用者,此次請求已經開始了。系統會使用另外的資源或者線程來完成此次調用操做,並在完成的時候知會調用者(好比經過回調函數)。拿Windows的ReadFile()或者POSIX的aio_read()來講,調用它以後,函數當即返回,操做系統在後臺同時開始讀操做。web

在以上三種IO形式中,非阻塞異步是性能最高、伸縮性最好的。算法

這篇文章探討不一樣的I/O利用機制並提供一種跨平臺的設計模式(解決方案). 但願此文能夠給於TCP高性能服務器開發者一些幫助,選擇最佳的設計方案。下面咱們會比較 Java, c#, C++各自對探討方案的實現以及性能. 咱們在文章的後面就再也不說起阻塞式的方案了,由於阻塞式I/O實在是缺乏可伸縮性,性能也達不到高性能服務器的要求。c#

兩種IO多路複用方案:Reactor and Proactor設計模式

通常狀況下,I/O 複用機制須要事件分享器(event demultiplexor [13]). 事件分享器的做用,即將那些讀寫事件源分發給各讀寫事件的處理者,就像送快遞的在樓下喊: 誰的什麼東西送了, 快來拿吧。開發人員在開始的時候須要在分享器那裏註冊感興趣的事件,並提供相應的處理者(event handlers),或者是回調函數; 事件分享器在適當的時候會將請求的事件分發給這些handler或者回調函數.api

涉及到事件分享器的兩種模式稱爲:Reactor and Proactor [1]. Reactor模式是基於同步I/O的,而Proactor模式是和異步I/O相關的. 在Reactor模式中,事件分離者等待某個事件或者可應用或個操做的狀態發生(好比文件描述符可讀寫,或者是socket可讀寫),事件分離者就把這個 事件傳給事先註冊的事件處理函數或者回調函數,由後者來作實際的讀寫操做。

而在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操做(至關於請求),而實際的工做是由操做系統來完成的。發起 時,須要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分離者得知了這 個請求,它默默等待這個請求的完成,而後轉發完成事件給相應的事件處理者或者回調。舉例來講,在Windows上事件處理者投遞了一個異步IO操做(稱有 overlapped的技術),事件分離者等IOCompletion事件完成[1]. 這種異步模式的典型實現是基於操做系統底層異步API的,因此咱們可稱之爲「系統級別」的或者「真正意義上」的異步,由於具體的讀寫是由操做系統代勞的。

舉另外個例子來更好地理解Reactor與Proactor兩種模式的區別。這裏咱們只關注read操做,由於write操做也是差很少的。下面是Reactor的作法:

  • 某個事件處理者宣稱它對某個socket上的讀事件很感興趣;
  • 事件分離者等着這個事件的發生;
  • 當事件發生了,事件分離器被喚醒,這負責通知先前那個事件處理者;
  • 事件處理者收到消息,因而去那個socket上讀數據了. 若是須要,它再次宣稱對這個socket上的讀事件感興趣,一直重複上面的步驟;

下面再來看看真正意義的異步模式Proactor是如何作的:

  • 事件處理者直接投遞發一個寫操做(固然,操做系統必須支持這個異步操做). 這個時候,事件處理者根本不關心讀事件,它只管發這麼個請求,它魂牽夢縈的是這個寫操做的完成事件。這個處理者很拽,發個命令就無論具體的事情了,只等着別人(系統)幫他搞定的時候給他回個話。
  • 事件分離者等着這個讀事件的完成(比較下與Reactor的不一樣);
  • 當事件分離者默默等待完成事情到來的同時,操做系統已經在一邊開始幹活了,它從目標讀取數據,放入用戶提供的緩存區中,最後通知事件分離者,這個事情我搞完了;
  • 事件分享者通知以前的事件處理者: 你吩咐的事情搞定了;
  • 事件處理者這時會發現想要讀的數據已經乖乖地放在他提供的緩存區中,想怎麼處理都行了。若是有須要,事件處理者還像以前同樣發起另一個寫操做,和上面的幾個步驟同樣。

現行作法

開源C++開發框架 ACE[13](Douglas Schmidt, et al.開發) 提供了大量平臺獨立的底層併發支持類(線程、互斥量等). 同時在更高一層它也提供了獨立的幾組C++類,用於實現Reactor及Proactor模式。 儘管它們都是平臺獨立的單元,但他們都提供了不一樣的接口.

ACE Proactor在MS-Windows上不管是性能還在健壯性都更勝一籌,這主要是因爲Windows提供了一系列高效的底層異步API. [45].

(這段可能過期了點吧) 不幸的是,並非全部操做系統都爲底層異步提供健壯的支持。舉例來講, 許多Unix系統就有麻煩.所以, ACE Reactor多是Unix系統上更合適的解決方案. 正由於系統底層的支持力度不一,爲了在各系統上有更好的性能,開發者不得不維護獨立的好幾份代碼: 爲Windows準備的ACE Proactor以及爲Unix系列提供的ACE Reactor.

就像咱們提到過的,真正的異步模式須要操做系統級別的支持。因爲事件處理者及操做系統交互的差別,爲Reactor和Proactor設計一種通用統一的外部接口是很是困難的。這也是設計通行開發框架的難點所在。

更好的解決方案

在文章這一段時,咱們將嘗試提供一種融合了Proactor和Reactor兩種模式的解決方案. 爲了演示這個方案,咱們將Reactor稍作調整,模擬成異步的Proactor模型(主要是在事件分離器裏完成本該事件處理者作的實際讀寫工做,咱們稱這種方法爲"模擬異步")。 下面的示例能夠看看read操做是如何完成的:

  • 事件處理者宣稱對讀事件感興趣,並提供了用於存儲結果的緩存區、讀數據長度等參數;
  • 調試者等待(好比經過select());
  • 當有事件到來(便可讀),調試者被喚醒, 調試者去執行非阻塞的讀操做(前面事件處理者已經給了足夠的信息了)。讀完後,它去通知事件處理者。
  • 事件處理者這時被知會讀操做已完成,它擁有完整的原先想要獲取的數據了.

咱們看到,經過爲分離者(也就上面的調試者)添加一些功能,可讓Reactor模式轉換爲Proactor模式。全部這些被執行的操做,實際上是和 Reactor模型應用時徹底一致的。咱們只是把工做打散分配給不一樣的角色去完成而已。這樣並不會有額外的開銷,也不會有性能上的的損失,咱們能夠再仔細 看看下面的兩個過程,他們實際上完成了同樣的事情:

標準的經典的 Reactor模式:

  • 步驟 1) 等待事件 (Reactor 的工做)
  • 步驟 2) 發"已經可讀"事件發給事先註冊的事件處理者或者回調 ( Reactor 要作的)
  • 步驟 3) 讀數據 (用戶代碼要作的)
  • 步驟 4) 處理數據 (用戶代碼要作的)

模擬的Proactor模式:

  • 步驟 1) 等待事件 (Proactor 的工做)
  • 步驟 2) 讀數據(看,這裏變成成了讓 Proactor 作這個事情)
  • 步驟 3) 把數據已經準備好的消息給用戶處理函數,即事件處理者(Proactor 要作的)
  • 步驟 4) 處理數據 (用戶代碼要作的)

在沒有底層異步I/O API支持的操做系統,這種方法能夠幫咱們隱藏掉socket接口的差別(不管是性能仍是其它), 提供一個徹底可用的統一"異步接口"。這樣咱們就能夠開發真正平臺獨立的通用接口了。

TProactor

咱們提出的TProactor方案已經由TerabitP/L [6]公司實現了. 它有兩種實現: C++的和Java的.C++版本使用了ACE平臺獨立的底層元件,最終在全部操做系統上提供了統一的異步接口。

TProactor中最重要的組件要數Engine和WaitStrategy了. Engine用於維護異步操做的生命週期;而WaitStrategy用於管理併發策略. WaitStrategy和Engine通常是成對出現的, 二者間提供了良好的匹配接口.

Engines和等待策略被設計成高度可組合的(完整的實現列表請參照附錄1)。TProactor是高度可配置的方案,經過使用異步內核API和同步Unix API(select()poll(), /dev/poll (Solaris 5.8+), port_get (Solaris 5.10),RealTime (RT) signals (Linux 2.4+), epoll (Linux 2.6), k-queue (FreeBSD) ),它內部實現了三種引擎(POSIX AIO, SUN AIO and Emulated AIO)並隱藏了六類等待策略。TProactor實現了和標準的 ACE Proactor同樣的接口。這樣一來,爲不一樣平臺提供通用統一的只有一份代碼的跨平臺解決方案成爲可能。

Engines和WaitStrategies能夠像樂高積木同樣自由地組合,開發者能夠在運行時經過配置參數來選擇合適的內部機制(引擎和等待策略)。 能夠根據需求設定配置,好比鏈接數,系統伸縮性,以及運行的操做系統等。若是系統支持相應的異步底層API,開發人員能夠選擇真正的異步策略,不然用戶也 能夠選擇使用模擬出來的異步模式。全部這一切策略上的實現細節都不太須要關注,咱們看到的是一個可用的異步模型。

舉例來講,對於運行在Sun Solaris上的HTTP服務器,若是須要支持大量的鏈接數,/dev/poll或者port_get()之類的引擎是比較合適的選擇;若是須要高吞吐 量,那使用基本select()的引擎會更好。因爲不一樣選擇策略內在算法的問題,像這樣的彈性選擇是標準ACE Reactor/Proactor模式所沒法提供的(見附錄2)。

在性能方面,咱們的測試顯示,模擬異步模式並未形成任何開銷,沒有變慢,反卻是性能有所提高。根據咱們的測試結果,TProactor相較標籤的ACE Reactor在Unix/Linux系統上有大約10-35%性能提高,而在Windows上差很少(測試了吞吐量及響應時間)。

性能比較 (JAVA / C++ / C#).

除了C++,咱們也在Java中實現了TProactor. JDK1.4中, Java僅提供了同步方法, 像C中的select() [78]. Java TProactor基於Java的非阻塞功能(java.nio包),相似於C++的TProactor使用了select()引擎.

圖一、2顯示了以 bits/sec爲單位的傳輸速度以及相應的鏈接數。這些圖比較瞭如下三種方式實現的echo服務器:標準ACE Reactor實現(基於RedHat Linux9.0)、TProactor C++/Java實現(Microsoft Windows平臺及RedHat v9.0), 以及C#實現。測試的時候,三種服務器使用相同的客戶端瘋狂地鏈接,不間斷地發送固定大小的數據包。

這幾組測試是在相同的硬件上作的,在不一樣硬件上作的相對結果對比也是相似。

圖 1. Windows XP/P4 2.6GHz HyperThreading/512 MB RAM.
圖 2. Linux RedHat 2.4.20-smp/P4 2.6GHz HyperThreading/512 MB RAM.

用戶代碼示例

下面是TProactor Java實現的echo服務器代碼框架。總的來講,開發者只須要實現兩個接口:一是OpRead,提供存放讀結果的緩存;二是OpWrite,提供存儲待 寫數據的緩存區。同時,開發者須要經過回調onReadComplated()和onWriteCompleted()實現協議相關的業務代碼。這些回調 會在合適的時候被調用.

 
class EchoServerProtocol implements AsynchHandler
{

AsynchChannel achannel = null;

EchoServerProtocol( Demultiplexor m, SelectableChannel channel )
throws Exception
{
this.achannel = new AsynchChannel( m, this, channel );
}

public void start() throws Exception
{
// called after construction
System.out.println( Thread.currentThread().getName() +
": EchoServer protocol started" );
achannel.read( buffer);
}

public void onReadCompleted( OpRead opRead ) throws Exception
{
if ( opRead.getError() != null )
{
// handle error, do clean-up if needed
System.out.println( "EchoServer::readCompleted: " +
opRead.getError().toString());
achannel.close();
return;
}

if ( opRead.getBytesCompleted () <= 0)
{
System.out.println("EchoServer::readCompleted: Peer closed "
+ opRead.getBytesCompleted();
achannel.close();
return;
}

ByteBuffer buffer = opRead.getBuffer();

achannel.write(buffer);
}

public void onWriteCompleted(OpWrite opWrite)
throws Exception
{
// logically similar to onReadCompleted
...
}
}

結束語

TProactor爲多個平臺提供了一個通用、彈性、可配置的高性能通信組件,全部那些在附錄2中提到的問題都被很好地隱藏在內部實現中了。

從上面的圖中咱們能夠看出C++仍舊是編寫高性能服務器最佳選擇,雖然Java已緊隨其後。然而由於Java自己實現上的問題,其在Windows上表現不佳(這已經應該成爲歷史了吧)。

須要注意的是,以上針對Java的測試,都是以裸數據的形式測試的,未涉及到數據的處理(影響性能)。

縱觀AIO在Linux上的快速發展[9], 咱們能夠預計Linux內核API將會提供大量更增強健的異步API, 如此一來之後基於此而實現的新的Engine/等待策略將能輕鬆地解決能用性方面的問題,而且這也能讓標準ACE Proactor接口受益。

附錄 I

TProactor中實現的Engines 和 等待策略

 

引擎類型 等待策略 操做系統
POSIX_AIO (true async)
aio_read()/aio_write()
aio_suspend()
Waiting for RT signal
Callback function
POSIX complained UNIX (not robust)
POSIX (not robust)
SGI IRIX, LINUX (not robust)
SUN_AIO (true async)
aio_read()/aio_write()
aio_wait() SUN (not robust)
Emulated Async
Non-blocking read()/write()
select()
poll()
/dev/poll
Linux RT signals
Kqueue
generic POSIX
Mostly all POSIX implementations
SUN
Linux
FreeBSD

 

附錄 II

全部同步等待策略可劃分爲兩組:

  • edge-triggered (e.g. Linux實時信號) - signal readiness only when socket became ready (changes state);
  • level-triggered (e.g. select()poll(), /dev/poll) - readiness at any time.

讓咱們看看這兩組的一些廣泛的邏輯問題:

  • edge-triggered group: after executing I/O operation, the demultiplexing loop can lose the state of socket readiness. Example: the "read" handler did not read whole chunk of data, so the socket remains still ready for read. But the demultiplexor loop will not receive next notification.
  • level-triggered group: when demultiplexor loop detects readiness, it starts the write/read user defined handler. But before the start, it should remove socket descriptior from theset of monitored descriptors. Otherwise, the same event can be dispatched twice.
  • Obviously, solving these problems adds extra complexities to development. All these problems were resolved internally within TProactor and the developer should not worry about those details, while in the synch approach one needs to apply extra effort to resolve them.

資源

[1] Douglas C. Schmidt, Stephen D. Huston "C++ Network Programming." 2002, Addison-Wesley ISBN 0-201-60464-7

[2] W. Richard Stevens "UNIX Network Programming" vol. 1 and 2, 1999, Prentice Hill, ISBN 0-13- 490012-X

[3] Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann "Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects, Volume 2" Wiley & Sons, NY 2000

[4] INFO: Socket Overlapped I/O Versus Blocking/Non-blocking Mode. Q181611. Microsoft Knowledge Base Articles.

[5] Microsoft MSDN. I/O Completion Ports.
http://msdn.microsoft.com/library/default.asp?url=/library/en- us/fileio/fs/i_o_completion_ports.asp

[6] TProactor (ACE compatible Proactor).
www.terabit.com.au

[7] JavaDoc java.nio.channels
http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/package-summary.html

[8] JavaDoc Java.nio.channels.spi Class SelectorProvider 
http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/spi/SelectorProvider.html

[9] Linux AIO development 
http://lse.sourceforge.net/io/aio.html, and
http://archive.linuxsymposium.org/ols2003/Proceedings/All-Reprints/Reprint-Pulavarty-OLS2003.pdf

更多

Ian Barile "I/O Multiplexing & Scalable Socket Servers", 2004 February, DDJ 

Further reading on event handling
- http://www.cs.wustl.edu/~schmidt/ACE-papers.html

The Adaptive Communication Environment
http://www.cs.wustl.edu/~schmidt/ACE.html

Terabit Solutions
http://terabit.com.au/solutions.php

關於做者

Alex Libman has been programming for 15 years. During the past 5 years his main area of interest is pattern-oriented multiplatform networked programming using C++ and Java. He is big fan and contributor of ACE.

Vlad Gilbourd works as a computer consultant, but wishes to spend more time listening jazz :) As a hobby,he started and runs www.corporatenews.com.au website.

相關文章
相關標籤/搜索