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采用過又放棄了, 據說是性能並沒有多路復用好.