JAVA高性能I/O設計模式


Java中的IO方式

主要分為3種:BIO(同步阻塞)、NIO(同步非阻塞)和AIO(異步非阻塞)。

BIO

同步阻塞模式。在JDK1.4以前,使用Java建立網絡連接時,只能采用BIO方式,在服務器端啟動一個ServerSocket,然后使用accept等待客戶端請求,對於每一個請求,使用一個線程來進行處理用戶請求。線程的大部分時間都在等待請求的到來和IO操作,利用率很低。而且線程的開銷比較大,數量有限,因此服務器同時能處理的連接數也很低。

NIO

BIO模式中,是“一個Socket一個線程”;而在NIO中則是使用單個或少量的線程來輪詢Socket,當發現Socket上有請求時,才為請求分配線程。因此是“一個請求一個線程”。

具體實現就是把Socket通過Channel注冊到Selector,使用一個線程在Selector中輪詢,發現Channel有讀寫的事件,就可以分配給其他線程來處理(通常使用線程池)。

AIO

從JDK7開始支持AIO模式。通過AsynchronousServerSocketChannel中注冊事件回調函數來處理業務邏輯。當IO操作完成以后,回調函數會被調用。如果傳入AsynchronousChannelGroup,可以綁定線程池來處理事件。

關於JDK的實現,Windows平台基於IOCP實現AIO,Linux只有eppoll模擬實現了AIO。

 

用一句話來總結這三種IO的區別:

  • BIO是一個連接一個線程。
  • NIO是一個請求一個線程。
  • AIO是一個有效請求一個線程。

IO中的幾個概念

以銀行取款為例: 

  • 同步 : 自己親自出馬持銀行卡到銀行取錢(使用同步IO時,Java自己處理IO讀寫);
  • 異步 : 委托一小弟拿銀行卡到銀行取錢,然后給你(使用異步IO時,Java將IO讀寫委托給OS處理,需要將數據緩沖區地址和大小傳給OS(銀行卡和密碼),OS需要支持異步IO操作API);
  • 阻塞 : ATM排隊取款,你只能等待(使用阻塞IO時,Java調用會一直阻塞到讀寫完成才返回);
  • 非阻塞 : 櫃台取款,取個號,然后坐在椅子上做其它事,等號廣播會通知你辦理,沒到號你就不能去,你可以不斷問大堂經理排到了沒有,大堂經理如果說還沒到你就不能去(使用非阻塞IO時,如果不能讀寫Java調用會馬上返回,當IO事件分發器會通知可讀寫時再繼續進行讀寫,不斷循環直到讀寫完成)

 系統I/O 可分為阻塞型, 非阻塞同步型以及非阻塞異步型。

阻塞型I/O意味着控制權只到調用操作結束了才會回到調用者手里。 結果調用者被阻塞了, 這段時間了做不了任何其它事情。 更郁悶的是,在等待IO結果的時間里,調用者所在線程此時無法騰出手來去響應其它的請求,這真是太浪費資源了。拿read()操作來說吧, 調用此函數的代碼會一直僵在此處直至它所讀的socket緩存中有數據到來。

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

在非阻塞異步調用中,稍有不同。調用函數在立即返回時,還告訴調用者,這次請求已經開始了。系統會使用另外的資源或者線程來完成這次調用操作,並在完成的時候知會調用者(比如通過回調函數)。拿Windows的ReadFile()或者POSIX的aio_read()來說,調用它之后,函數立即返回,操作系統在后台同時開始讀操作。

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

Reactor and Proactor

一般情況下,I/O 復用機制需要事件分享器(event demultiplexor )。 事件分享器的作用,即將那些讀寫事件源分發給各讀寫事件的處理者,就像送快遞的在樓下喊: 誰的什么東西送了, 快來拿吧。開發人員在開始的時候需要在分享器那里注冊感興趣的事件,並提供相應的處理者(event handlers),或者是回調函數; 事件分享器在適當的時候會將請求的事件分發給這些handler或者回調函數。

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

Reactor模式:

Reator類圖如上所示,Reactor模式又叫反應器或反應堆,即實現注冊描述符(Handle)及事件的處理器(EventHandler),當有事件發生的時候,事件多路分發器(Event Demultiplexer)做出反應,調用事件具體的處理函數(ConcreteEventHandler::handle_event())。

Reator模式的典型啟動過程如下:

  1. 創建Reactor
  2. 注冊事件處理器(Reactor::register_handler())
  3. 調用事件多路分發器進入無限事件循環(Reacor:handle_events)
  4. 當操作系統通知某描述符狀態就緒時,事件多路分發器找出並調用此描述符注冊的事件處理器。

 Reactor模式已經被廣泛使用,著名的開源事件庫libevent、libev、libuv都是使用Reactor模式。

Proactor模式

Proactor模式的類圖如上圖所示,Proactor模式又叫前攝器或主動器模式。它用於實現異步I/O模型,運行流程如下:

  1. Initiator主動調用Asynchronous Operation Processor發起異步I/O操作,
  2. 記錄異步操作的參數和函數地址放入完成事件隊列(Completion Event Queue)中
  3. Proactor循環檢測異步事件是否完成。如果完成則從完成事件隊列中取出回調函數完成回調。

Boost庫中的asio就使用了Proactor模式,其底層的異步I/O由操作系統提供,而異步事件的分發還是由epoll/kequeue/select等實現。

在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操作(相當於請求),而實際的工作是由操作系統來完成的。發起時,需要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完后的回調函數等信息。事件分離者得知了這個請求,它默默等待這個請求的完成,然后轉發完成事件給相應的事件處理者或者回調。

兩者區別

主要區別:

  • Reactor實現同步I/O多路分發,Proactor實現異步I/O分發。

如果只是處理網絡I/O單線程的Reactor尚可處理,但如果涉及到文件I/O,單線程的Reactor可能被文件I/O阻塞而導致其他事件無法被分發。所以涉及到文件I/O最好還是使用Proactor模式,或者用多線程模擬實現異步I/O的方式。

  • Reactor模式注冊的是文件描述符的就緒事件,而Proactor模式注冊的是完成事件。

即Reactor模式有事件發生的時候要判斷是讀事件還是寫事件,然后用再調用系統調用(read/write等)將數據從內核中拷貝到用戶數據區繼續其他業務處理。

而Proactor模式一般使用的是操作系統的異步I/O接口,發起異步調用(用戶提供數據緩沖區)之后操作系統將在內核態完成I/O並拷貝數據到用戶提供的緩沖區中,完成事件到達之后,用戶只需要實現自己后續的業務處理即可。

  • 主動和被動

Reactor模式是一種被動的處理,即有事件發生時被動處理。而Proator模式則是主動發起異步調用,然后循環檢測完成事件。

寫在最后

我們知道linux系統提供的異步I/O,只支持O_DIRECT,不能帶緩存。因此出現了開源庫libeio,它和Linux的異步I/O一樣也是用多線程模擬,但是更高效。下圖是libeio的異步I/O實現,是不是很像Proactor模式啊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM