IO多路復用模型
服務器端編程經常需要構造高性能的IO模型,常見的IO模型有四種:
(1)同步阻塞IO(Blocking IO):即傳統的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默認創建的socket都是阻塞的,非阻塞IO要求socket被設置為NONBLOCK。
(3)IO多路復用(IO Multiplexing):即經典的Reactor設計模式,有時也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
(4)異步IO(Asynchronous IO):即經典的Proactor設計模式,也稱為異步非阻塞IO。
此文主要講IO多路復用。
I/O是指網絡I/O
多路指多個TCP連接(即socket或者channel),復用指復用一個或幾個線程。
即:同一個線程內同時處理多個TCP連接。 最大優勢是減少系統開銷小,不必創建/維護過多的線程。
IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。
圖1 多路分離函數select
如圖1所示,用戶首先將需要進行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(反應堆)設計模式實現了這一機制。
圖2 Reactor設計模式
如圖2所示,EventHandler抽象類表示IO事件處理器,它擁有IO文件句柄Handle(通過get_handle獲取),以及對Handle的操作handle_event(讀/寫等)。
繼承EventHandler的子類可以對事件處理器的行為進行定制。
Reactor類用於管理EventHandler(註冊、刪除等),並使用handle_events實現事件循環,不斷調用同步事件多路分離器(一般是內核)的多路分離函數select,只要某個文件句柄被激活(可讀/寫等),select就返回(阻塞),handle_events就會調用與文件句柄關聯的事件處理器的handle_event進行相關操作。
圖3 IO多路復用
如圖3所示,通過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多路復用模型