在高性能的I/O設計中,有兩個比較著名的模式Reactor和Proactor模式,其中Reactor模式用於同步I/O,而Proactor運用於異步I/O操作。
在比較這兩個模式之前,我們首先的搞明白幾個概念,什么是阻塞和非阻塞,什么是同步和異步
?同步和異步是針對應用程序和內核的交互而言的,同步指的是用戶進程觸發IO操作並等待或者輪詢的去查看IO操作是否就緒,而異步是指用戶進程觸發IO操作以后便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知(異步的特點就是通知)。
而阻塞和非阻塞是針對於進程在訪問數據的時候,根據IO操作的就緒狀態來采取的不同方式,說白了是一種讀取或者寫入操作函數的實現方式,阻塞方式下讀取或者寫入函數將一直等待,而非阻塞方式下,讀取或者寫入函數會立即返回一個狀態值。
一般來說I/O模型可以分為:同步阻塞,同步非阻塞,異步阻塞,異步非阻塞IO
同步阻塞IO:
在此種方式下,用戶進程在發起一個IO操作以后,必須等待IO操作的完成,只有當真正完成了IO操作以后,用戶進程才能運行。JAVA傳統的IO模型屬於此種方式!
同步非阻塞IO:
在此種方式下,用戶進程發起一個IO操作以后邊可返回做其它事情,但是用戶進程需要時不時的詢問IO操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的CPU資源浪費。其中目前JAVA的NIO就屬於同步非阻塞IO。
異步阻塞IO:
此種方式下是指應用發起一個IO操作以后,不等待內核IO操作的完成,等內核完成IO操作以后會通知應用程序,這其實就是同步和異步最關鍵的區別,同步必須等待或者主動的去詢問IO是否完成,那么為什么說是阻塞的呢?因為此時是通過select系統調用來完成的,而select函數本身的實現方式是阻塞的,而采用select函數有個好處就是它可以同時監聽多個文件句柄(如果從UNP的角度看,select屬於同步操作。因為select之后,進程還需要讀寫數據),從而提高系統的並發性!
異步非阻塞IO:
在此種模式下,用戶進程只需要發起一個IO操作然后立即返回,等IO操作真正的完成以后,應用程序會得到IO操作完成的通知,此時用戶進程只需要對數據進行處理就好了,不需要進行實際的IO讀寫操作,因為真正的IO讀取或者寫入操作已經由內核完成了。目前Java中還沒有支持此種IO模型。
搞清楚了以上概念以后,我們再回過頭來看看,Reactor模式和Proactor模式。
(其實阻塞與非阻塞都可以理解為同步范疇下才有的概念,對於異步,就不會再去分阻塞非阻塞。對於用戶進程,接到異步通知后,就直接操作進程用戶態空間里的數據好了。)
首先來看看Reactor模式,Reactor模式應用於同步I/O的場景。我們分別以讀操作和寫操作為例來看看Reactor中的具體步驟:
讀取操作:
1. 應用程序注冊讀就緒事件和相關聯的事件處理器
2. 事件分離器等待事件的發生
3. 當發生讀就緒事件的時候,事件分離器調用第一步注冊的事件處理器
4. 事件處理器首先執行實際的讀取操作,然后根據讀取到的內容進行進一步的處理
寫入操作類似於讀取操作,只不過第一步注冊的是寫就緒事件。
下面我們來看看Proactor模式中讀取操作和寫入操作的過程:
讀取操作:
1. 應用程序初始化一個異步讀取操作,然后注冊相應的事件處理器,此時事件處理器不關注讀取就緒事件,而是關注讀取完成事件,這是區別於Reactor的關鍵。
2. 事件分離器等待讀取操作完成事件
3. 在事件分離器等待讀取操作完成的時候,操作系統調用內核線程完成讀取操作(異步IO都是操作系統負責將數據讀寫到應用傳遞進來的緩沖區供應用程序操作,操作系統扮演了重要角色),並將讀取的內容放入用戶傳遞過來的緩存區中。這也是區別於Reactor的一點,Proactor中,應用程序需要傳遞緩存區。
4. 事件分離器捕獲到讀取完成事件后,激活應用程序注冊的事件處理器,事件處理器直接從緩存區讀取數據,而不需要進行實際的讀取操作。
Proactor中寫入操作和讀取操作,只不過感興趣的事件是寫入完成事件。
從上面可以看出,Reactor和Proactor模式的主要區別就是真正的讀取和寫入操作是有誰來完成的,Reactor中需要應用程序自己讀取或者寫入數據,而Proactor模式中,應用程序不需要進行實際的讀寫過程,它只需要從緩存區讀取或者寫入即可,操作系統會讀取緩存區或者寫入緩存區到真正的IO設備.
綜上所述,同步和異步是相對於應用和內核的交互方式而言的,同步 需要主動去詢問,而異步的時候內核在IO事件發生的時候通知應用程序,而阻塞和非阻塞僅僅是系統在調用系統調用的時候函數的實現方式而已。
//////////////////////////////////////////////
說到阻塞,首先得說說I/O等待。I/O等待是不可避免的,那么既然有了等待,就會有阻塞,但是注意,我們說的阻塞是指當前發起I/O操作的進程被阻塞
同步阻塞I/O便是指,當進程調用某些涉及I/O操作的系統調用或庫函數時,比如accept()(注意accept也算在了i/o操作)、send()、recv()等,進程便暫停下來,等待I/O操作完成再繼續運行。這是一種簡單而有
效的I/O模型,它可以和多進程結合起來有效的利用CPU資源,但是代價就是多進程的大量內存開銷。
同步阻塞 進程坐水,就不能燒粥
同步非阻塞 類似於用一個進程坐水,燒粥. while(true){if... if... } 好處就是一個進程處理多個i/o請求. 劣勢就是需要不停的輪詢.
區別在於等不等待數據就緒. 因為數據占了等待的80%時間. 同步非阻塞的優勢就是一個進程里同時處理多個I/O操作。
在同步阻塞I/O中,進程實際上等待的時間可能包括兩部分,一個是等待數據的就緒,另一個是等待數
據的復制,對於網絡I/O來說,前者的時間可能要更長一些。
與此不同的是,同步非阻塞I/O的調用不會等待數據的就緒,如果數據不可讀或者不可寫,它會立即返
回告訴進程。
比如我們使用非阻塞recv()接收網絡數據的時候,如果網卡緩沖區中沒有可接收的數據,函數就及時返回,告訴進程沒有數據可讀了。相比於阻塞I/O,這種非阻塞I/O結合反復的輪詢來嘗試
數據是否就緒,防止進程被阻塞,最大的好處便在於可以在一個進程里同時處理多個I/O操作。但正是由於需要進程執行多次的輪詢來查看數據是否就緒,這花費了大量的CPU時間,使得進程處於忙碌等待狀態。
非阻塞I/O一般只針對網絡I/O有效,我們只要在socket的選項設置中使用O_NONBLOCK即可,這樣對於該socket的send()或recv()便采用非阻塞方式。
如果服務器想要同時接收多個TCP連接的數據,就必須輪流對每個socket調用接收數據的方法,比如recv()。不管這些socket有沒有可以接收的數據,都要詢問一遍,假如大部分socket並沒有數據可以接收,那么進程便會浪費很多CPU時間用於檢查這些socket,這顯然不是我們所希望看到的。
同步和異步,阻塞和非阻塞,有些混用,其實它們完全不是一回事,而且它們修飾的對象也不相同。
阻塞和非阻塞是指當進程訪問的數據如果尚未就緒,進程是否需要等待,簡單說這相當於函數內部的實現區別,也就是未就緒時是直接返回還是等待就緒;
而同步和異步是指訪問數據的機制,同步一般指主動請求並等待I/O操作完畢的方式,當數據就緒后在讀寫的時候必須阻塞(區別就緒與讀寫二個階段,同步的讀寫必須阻塞),異步則指主動請求數據后便可以繼續處理其它任務,隨后等待I/O,操作完畢的通知,這可以使進程在數據讀寫時也不阻塞。(等待"通知")
多數情況下,Web服務器對這些請求采用基於隊列的自由競爭,通過多執行流(多進程或多線程)來充分占 用CPU以及I/O資源,減少任何無辜的等待時間,這其中包括了很多種具體實現的並發策略,
在實際應用中,特別是Web服務器,同時處理大量的文件描述符是必不可少的.多路I/O就緒通知的出現,提供了對大量文件描述符就緒檢查的高性能方案,它允許進程(比如電子屏,會聞到各個飯館做好飯菜的味道)通過一種方法來同時監視所有文件描述符,並可以快速獲得所有就緒的文件描述符,然后只針對這些文件描述符進行數據訪問。
回到買面條的故事中,假如你不止買了一份面條,還在其它幾個小吃店買了餃子、粥、餡餅等,因為一起逛街的朋友看到你的面條后也餓了。這些東西都需要時間來等待制作。在同步非阻塞I/O模型中,你
要輪流不停的去各個小吃店詢問進度,痛苦不堪。現在引入多路I/O就緒通知后,小吃城管理處給大廳安裝了一塊電子屏幕,以后所有小吃店的食物做好后,都會顯示在屏幕上,這可真是個好消息,你只需
要間隔性的看看大屏幕就可以了,也許你還可以同時逛逛附近的商店,在不遠處也可以看到大屏幕。
多路就緒:1.強調多路. 2.只針對請求數據是否就緒.不針對i/o讀寫
epoll針對的是這樣的場景.
select, epoll都只需要進程(我)被動接收到數據就緒(面條)"通知".符合異步的定義. 不需要一直在飯館等(同步阻塞).或輪詢(同步非阻塞).
