四種常用IO模型


1) 同步阻塞IO(Blocking IO)
2) 同步非阻塞IO(Non-blocking IO)
3) IO多路復用(IO Multiplexing)
4) 異步IO(Asynchronous IO)

注意以下概念:

1.同步/異步

同步和異步是相對的
同步 前后兩件任務, 有嚴格的順序一致性(依賴和遞進), 按順序執行, 執行完一個再執行下一個, 需要等待、協調運行
異步 對順序的要求和依賴關系沒那么強, 表現出來就是兩個任務可以分給兩個人做, 在等待任務A結束時(同步點前)可以進行任務B
多線程就是實現異步的一個方式, 它把"第二件任務"交給其他的線程去做了. 硬件的DMA也是異步.

在實際編程中, 同步和異步區分了請求與響應的交互中, 獲取響應的方式
同步: 請求某種結果, 響應返回所需結果
異步: 請求'給我結果', 第一次響應回答'我知道了', 第二次響應通知請求線程'已完成' (通過狀態通知或調用請求者注冊的回調函數等方式)

2.阻塞/非阻塞

阻塞和非阻塞也是相對概念
阻塞   : 請求-響應比較耗時, 如IO操作
非阻塞: 請求-響應比較迅速, 如沒有等待IO完成就直接返回狀態值
socket的非阻塞IO需要設置為NONBLOCK

 

下列內容節選自 http://www.cnblogs.com/fanzhidongyzby/p/4098546.html, 感謝原作者!

1.同步阻塞IO

最簡單的IO模型,用戶線程在讀寫時被阻塞

數據拷貝指請求到的數據先存放在內核空間, 然后從內核空間拷貝至程序的緩沖區

偽代碼如下

{
// read阻塞 read(socket, buffer); // 處理buffer process(buffer); }

用戶線程在IO過程中被阻塞,不能做任何事情,對CPU的資源利用率不高

 

2. 同步非阻塞

用戶線程不斷發起IO請求. 數據未到達時系統返回一狀態值; 數據到達后才真正讀取數據

偽代碼如下

{
    // read非阻塞   
    while(read(socket, buffer) != SUCCESS);
    process(buffer);
}

用戶線程每次請求IO都可以立即返回,但是為了拿到數據,需不斷輪詢,無謂地消耗了大量的CPU
一般很少直接使用這種模型,而是在其他IO模型中使用非阻塞IO這一特性

3. IO多路復用

IO多路復用建立在內核提供的阻塞函數select上

用戶先將需要進行IO操作的socket添加到select中,然后等待阻塞函數select返回。當數據到達后,socket被激活,select返回,用戶線程就能接着發起read請求

偽代碼如下:

{
    // 注冊
    select(socket);
    // 輪詢
    while(true) {
        // 阻塞
        sockets = select();
        // 數據到達, 解除阻塞
        for(socket in sockets) {
            if(can_read(socket)) {
            // 數據已到達, 那么socket阻不阻塞無所謂
       read(socket, buffer); process(buffer); } } } }

看起來和加了循環的同步阻塞IO差不多?

實際上, 我們可以給select注冊多個socket, 然后不斷調用select讀取被激活的socket,實現在同一線程內同時處理多個IO請求的效果.

至此, 同步阻塞(阻塞在select) / 同步非阻塞(IO沒有阻塞) {不知道該怎么稱呼}完成

更進一步, 我們把select輪詢抽出來放在一個線程里, 用戶線程向其注冊相關socket或IO請求,等到數據到達時通知用戶線程,則可以提高用戶線程的CPU利用率.

這樣, 便實現了異步方式

這其實是Reactor設計模式, 如下圖

EventHandler抽象類表示IO事件處理器
   get_handle方法獲得文件句柄Handle
   handle_event方法實現對Handle的操作
可繼承EventHandler對事件處理器的行為進行定制


Reactor類管理EventHandler的注冊、刪除. handle_events方法實現了事件循環, 其不斷調用阻塞函數select, 只要某個文件句柄被激活(可讀/寫等),select就從阻塞中返回, handle_events接着調用與文件句柄關聯的事件處理器的handle_event進行相關操作。handler_events的偽代碼如下

Reactor::handle_events() {
    while(true) {
        sockets = select();
        for(socket in sockets) {
            get_event_handler(socket).handle_event();
        }
    }
}

作為功能調用者需要實現的偽代碼如下

// 繼承EventHandler並重寫handle_event()方法
void UserEventHandler::handle_event() {
    if(can_read(socket)) {
    // 數據已到達, 那么socket阻不阻塞無所謂
    read(socket, buffer);
    process(buffer);
    }
}
// 注冊實現的EventHandler子類
{
    Reactor.register(new UserEventHandler(socket));
}

IO多路復用是最常使用的IO模型,因其輪詢select的線程會被阻塞, 異步程度還不夠“徹底”, 所以常被稱為異步阻塞IO

 

4. 異步IO

真正的異步IO需要操作系統更強的支持。
IO多路復用模型中,數據到達內核后通知用戶線程,用戶線程負責從內核空間拷貝數據;
而在異步IO模型中,當用戶線程收到通知時,數據已經被操作系統從內核拷貝到用戶指定的緩沖區內,用戶線程直接使用即可。

異步IO模型使用了Proactor設計模式實現了這一機制。

Reactor模式中,用戶線程向Reactor對象注冊事件對應的事件處理器,然后事件觸發時Reactor調用事件處理函數。
Proactor模式中,用戶線程將AsynchronousOperation(讀/寫等)、Proactor以及操作完成時的CompletionHandler注冊到AsynchronousOperationProcessor。
AsynchronousOperationProcessor使用Facade模式提供了一組異步API(讀/寫等)供用戶調用. 當用戶線程調用異步API后,便繼續執行下一步代碼. 而此時AsynchronousOperationProcessor會開啟獨立的內核線程執行異步操作。
當read請求的數據到達時,由內核負責讀取socket中的數據,並寫入用戶指定的緩沖區中。
異步IO完成時,AsynchronousOperationProcessor將Proactor和CompletionHandler取出,並將IO操作結果和CompletionHandler分發給Proactor,Proactor通知用戶線程(即回調先前注冊的事件完成處理類的函數handle_event)。
Proactor一般被實現為單例,以便於集中分發操作完成事件。

偽代碼如下

// 繼承CompletionHandler, buffer為用戶線程指定的緩沖區
void UserCompletionHandler::handle_event(buffer) {
    process(buffer);
}

// 調用異步的read函數
{
    aio_read(socket, new UserCompletionHandler);
}

相比於IO多路復用,異步IO並不常用,因為目前操作系統對異步IO的支持並不完善,IO多路復用也基本夠用. 有很多做法是用IO多路復用模型模擬異步IO(IO事件觸發時不直接通知用戶線程,而是將數據讀寫完畢后放到用戶指定的緩沖區中)。
JDK7已經支持了AIO, netty采用過又放棄了, 據說是性能並沒有多路復用好.


免責聲明!

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



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