高效能IO模型淺析
伺服器端程式設計經常需要構造高效能的IO
模型,常見的IO
模型有四種:
(1)同步阻塞IO(Blocking IO
):即傳統的IO模型。
(2)同步非阻塞IO(Non-blocking IO
):預設建立的socket
都是阻塞的,非阻塞IO
要求socket
被設定為NONBLOCK
。注意這裡所說的NIO
並非Java
的NIO(New IO)
庫。
(3)IO多路複用(IO Multiplexing
):即經典的Reactor
設計模式,有時也稱為非同步阻塞IO,Java
中的Selector
和Linux
中的epoll
都是這種模型。
(4)非同步IO(Asynchronous IO
):即經典的Proactor
設計模式,也稱為非同步非阻塞IO。
同步和非同步的概念描述的是使用者執行緒與核心的互動方式:同步是指使用者執行緒發起IO
請求後需要等待或者輪詢核心IO
操作完成後才能繼續執行;而非同步是指使用者執行緒發起IO
請求後仍繼續執行,當核心IO
操作完成後會通知使用者執行緒,或者呼叫使用者執行緒註冊的回撥函式。
阻塞和非阻塞的概念描述的是使用者執行緒呼叫核心IO
操作的方式:阻塞是指IO
操作需要徹底完成後才返回到使用者空間;而非阻塞是指IO
操作被呼叫後立即返回給使用者一個狀態值,無需等到IO
操作徹底完成。
另外,Richard Stevens
在《Unix 網路程式設計》卷1中提到的基於訊號驅動的IO(Signal Driven IO
)模型,由於該模型並不常用,本文不作涉及。接下來,我們詳細分析四種常見的IO
模型的實現原理。為了方便描述,我們統一使用IO
的讀操作作為示例。
一、同步阻塞IO
同步阻塞IO
模型是最簡單的IO
模型,使用者執行緒在核心進行IO
操作時被阻塞。
圖1 同步阻塞IO
如圖1所示,使用者執行緒通過系統呼叫read
發起IO
讀操作,由使用者空間轉到核心空間。核心等到資料包到達後,然後將接收的資料拷貝到使用者空間,完成read
操作。
使用者執行緒使用同步阻塞IO
模型的虛擬碼描述為:
{
read(socket, buffer);
process(buffer);
}
即使用者需要等待read
將socket
中的資料讀取到buffer
後,才繼續處理接收的資料。整個IO
請求的過程中,使用者執行緒是被阻塞的,這導致使用者在發起IO
請求時,不能做任何事情,對CPU
的資源利用率不夠。
二、同步非阻塞IO
同步非阻塞IO
是在同步阻塞IO
的基礎上,將socket
設定為NONBLOCK
。這樣做使用者執行緒可以在發起IO
請求後可以立即返回。
圖2 同步非阻塞IO
如圖2所示,由於socket
是非阻塞的方式,因此使用者執行緒發起IO
請求時立即返回。但並未讀取到任何資料,使用者執行緒需要不斷地發起IO
請求,直到資料到達後,才真正讀取到資料,繼續執行。
使用者執行緒使用同步非阻塞IO
模型的虛擬碼描述為:
{
while(read(socket, buffer) != SUCCESS);
process(buffer);
}
即使用者需要不斷地呼叫read
,嘗試讀取socket
中的資料,直到讀取成功後,才繼續處理接收的資料。整個IO
請求的過程中,雖然使用者執行緒每次發起IO
請求後可以立即返回,但是為了等到資料,仍需要不斷地輪詢、重複請求,消耗了大量的CPU
的資源。一般很少直接使用這種模型,而是在其他IO
模型中使用非阻塞IO
這一特性。
三、IO多路複用
IO
多路複用模型是建立在核心提供的多路分離函式select
基礎之上的,使用select
函式可以避免同步非阻塞IO
模型中輪詢等待的問題。
圖3 多路分離函式select
如圖3所示,使用者首先將需要進行IO
操作的socket
新增到select
中,然後阻塞等待select
系統呼叫返回。當資料到達時,socket
被啟用,select
函式返回。使用者執行緒正式發起read
請求,讀取資料並繼續執行。
從流程上來看,使用select
函式進行IO
請求和同步阻塞模型沒有太大的區別,甚至還多了新增監視socket
,以及呼叫select
函式的額外操作,效率更差。但是,使用select
以後最大的優勢是使用者可以在一個執行緒內同時處理多個socket
的IO
請求。使用者可以註冊多個socket
,然後不斷地呼叫select
讀取被啟用的socket
,即可達到在同一個執行緒內同時處理多個IO
請求的目的。而在同步阻塞模型中,必須通過多執行緒的方式才能達到這個目的。
使用者執行緒使用select
函式的虛擬碼描述為:
{
select(socket);
while(1) {
sockets = select();
for(socket in sockets) {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
}
}
其中while
迴圈前將socket
新增到select
監視中,然後在while
內一直呼叫select
獲取被啟用的socket
,一旦socket
可讀,便呼叫read
函式將socket
中的資料讀取出來。
然而,使用select
函式的優點並不僅限於此。雖然上述方式允許單執行緒內處理多個IO
請求,但是每個IO
請求的過程還是阻塞的(在select
函式上阻塞),平均時間甚至比同步阻塞IO
模型還要長。如果使用者執行緒只註冊自己感興趣的socket
或者IO
請求,然後去做自己的事情,等到資料到來時再進行處理,則可以提高CPU
的利用率。
IO
多路複用模型使用了Reactor
設計模式實現了這一機制。
圖4 Reactor設計模式
如圖4所示,EventHandler
抽象類表示IO
事件處理器,它擁有IO
檔案控制程式碼Handle
(通過get_handle
獲取),以及對Handle
的操作handle_event
(讀/寫等)。繼承於EventHandler
的子類可以對事件處理器的行為進行定製。Reactor
類用於管理EventHandler
(註冊、刪除等),並使用handle_events
實現事件迴圈,不斷呼叫同步事件多路分離器(一般是核心)的多路分離函式select
,只要某個檔案控制程式碼被啟用(可讀/寫等),select
就返回(阻塞),handle_events
就會呼叫與檔案控制程式碼關聯的事件處理器的handle_event
進行相關操作。
圖5 IO多路複用
如圖5所示,通過Reactor
的方式,可以將使用者執行緒輪詢IO
操作狀態的工作統一交給handle_events
事件迴圈進行處理。使用者執行緒註冊事件處理器之後可以繼續執行做其他的工作(非同步),而Reactor
執行緒負責呼叫核心的select
函式檢查socket
狀態。當有socket
被啟用時,則通知相應的使用者執行緒(或執行使用者執行緒的回撥函式),執行handle_event
進行資料讀取、處理的工作。由於select
函式是阻塞的,因此多路IO
複用模型也被稱為非同步阻塞IO
模型。注意,這裡的所說的阻塞是指select
函式執行時執行緒被阻塞,而不是指socket
。一般在使用IO
多路複用模型時,socket
都是設定為NONBLOCK
的,不過這並不會產生影響,因為使用者發起IO
請求時,資料已經到達了,使用者執行緒一定不會被阻塞。
使用者執行緒使用IO
多路複用模型的虛擬碼描述為:
void UserEventHandler::handle_event() {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
{
Reactor.register(new UserEventHandler(socket));
}
使用者需要重寫EventHandler
的handle_event
函式進行讀取資料、處理資料的工作,使用者執行緒只需要將自己的EventHandler
註冊到Reactor
即可。Reactor
中handle_events
事件迴圈的虛擬碼大致如下。
Reactor::handle_events() {
while(1) {
sockets = select();
for(socket in sockets) {
get_event_handler(socket).handle_event();
}
}
}
事件迴圈不斷地呼叫select
獲取被啟用的socket
,然後根據獲取socket
對應的EventHandler
,執行器handle_event
函式即可。
IO
多路複用是最常使用的IO
模型,但是其非同步程度還不夠“徹底”,因為它使用了會阻塞執行緒的select
系統呼叫。因此IO
多路複用只能稱為非同步阻塞IO
,而非真正的非同步IO
。
四、非同步IO
“真正”的非同步IO
需要作業系統更強的支援。在IO
多路複用模型中,事件迴圈將檔案控制程式碼的狀態事件通知給使用者執行緒,由使用者執行緒自行讀取資料、處理資料。而在非同步IO
模型中,當使用者執行緒收到通知時,資料已經被核心讀取完畢,並放在了使用者執行緒指定的緩衝區內,核心在IO
完成後通知使用者執行緒直接使用即可。
非同步IO
模型使用了Proactor
設計模式實現了這一機制。
圖6 Proactor設計模式
如圖6,Proactor
模式和Reactor
模式在結構上比較相似,不過在使用者(Client
)使用方式上差別較大。Reactor
模式中,使用者執行緒通過向Reactor
物件註冊感興趣的事件監聽,然後事件觸發時呼叫事件處理函式。而Proactor
模式中,使用者執行緒將AsynchronousOperation
(讀/寫等)、Proactor
以及操作完成時的CompletionHandler
註冊到AsynchronousOperationProcessor
。AsynchronousOperationProcessor
使用Facade
模式提供了一組非同步操作API
(讀/寫等)供使用者使用,當使用者執行緒呼叫非同步API
後,便繼續執行自己的任務。AsynchronousOperationProcessor
會開啟獨立的核心執行緒執行非同步操作,實現真正的非同步。當非同步IO
操作完成時,AsynchronousOperationProcessor
將使用者執行緒與AsynchronousOperation
一起註冊的Proactor
和CompletionHandler
取出,然後將CompletionHandler
與IO
操作的結果資料一起轉發給Proactor
,Proactor
負責回撥每一個非同步操作的事件完成處理函式handle_event
。雖然Proactor
模式中每個非同步操作都可以繫結一個Proactor
物件,但是一般在作業系統中,Proactor
被實現為Singleton
模式,以便於集中化分發操作完成事件。
圖7 非同步IO
如圖7所示,非同步IO
模型中,使用者執行緒直接使用核心提供的非同步IO API發起read
請求,且發起後立即返回,繼續執行使用者執行緒程式碼。不過此時使用者執行緒已經將呼叫的AsynchronousOperation
和CompletionHandler
註冊到核心,然後作業系統開啟獨立的核心執行緒去處理IO
操作。當read
請求的資料到達時,由核心負責讀取socket
中的資料,並寫入使用者指定的緩衝區中。最後核心將read
的資料和使用者執行緒註冊的CompletionHandler
分發給內部Proactor
,Proactor
將IO
完成的資訊通知給使用者執行緒(一般通過呼叫使用者執行緒註冊的完成事件處理函式),完成非同步IO
。
使用者執行緒使用非同步IO
模型的虛擬碼描述為:
void UserCompletionHandler::handle_event(buffer) {
process(buffer);
}
{
aio_read(socket, new UserCompletionHandler);
}
使用者需要重寫CompletionHandler
的handle_event
函式進行處理資料的工作,引數buffer
表示Proactor
已經準備好的資料,使用者執行緒直接呼叫核心提供的非同步IO API
,並將重寫的CompletionHandler
註冊即可。
相比於IO
多路複用模型,非同步IO
並不十分常用,不少高效能併發服務程式使用IO
多路複用模型+多執行緒任務處理的架構基本可以滿足需求。況且目前作業系統對非同步IO
的支援並非特別完善,更多的是採用IO
多路複用模型模擬非同步IO
的方式(IO
事件觸發時不直接通知使用者執行緒,而是將資料讀寫完畢後放到使用者指定的緩衝區中)。Java7
之後已經支援了非同步IO
,感興趣的讀者可以嘗試使用。
本文從基本概念、工作流程和程式碼示例三個層次簡要描述了常見的四種高效能IO
模型的結構和原理,理清了同步、非同步、阻塞、非阻塞這些容易混淆的概念。通過對高效能IO
模型的理解,可以在服務端程式的開發中選擇更符合實際業務特點的IO
模型,提高服務質量。希望本文對你有所幫助。
相關文章
- Socket高效能IO模型淺析模型
- NIO(二)淺析IO模型模型
- java.io.Serializable淺析Java
- Flutter 高效能原理淺析Flutter
- 淺析java中的IO流Java
- 主題模型-LDA淺析模型LDA
- java 淺析I/O模型Java模型
- Redis基礎篇(二)高效能IO模型Redis模型
- 常見的索引模型淺析索引模型
- Java NIO:淺析I/O模型Java模型
- 雲端高效能技術架構淺析架構
- IO多路複用與epoll機制淺析
- Java 記憶體模型 JMM 淺析Java記憶體模型
- java的nio之:淺析I/O模型Java模型
- 三維模型調色技術淺析模型
- 淺析三維模型輕量化技術方法模型
- IO模型模型
- 淺析虛擬機器記憶體管理模型虛擬機記憶體模型
- IO模型學習(一)IO模型分類模型
- iOS Block淺淺析iOSBloC
- Qt5MV自定義模型與例項淺析QT模型
- System.IO.Pipelines: .NET高效能IO
- 淺析 JWTJWT
- MongoDB淺析MongoDB
- RunLoop 淺析OOP
- Nginx淺析Nginx
- 淺析 requestAnimationFramerequestAnimationFrame
- 淺析PromisePromise
- 淺析GitGit
- 淺析RedisRedis
- Jvm 淺析JVM
- 深入淺出IO
- Actor模型淺析 一致性和隔離性模型
- 淺析大模型在銀行業客服中心的應用大模型行業
- LLM大模型向量資料庫技術架構淺析大模型資料庫架構
- Linux IO模型Linux模型
- io模型 WSAAsyncSelect模型
- Webpack 原理淺析Web