常見的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。
(5)基於信號驅動的IO(Signal Driven IO)模型,由於該模型並不常用(略)
在理解關於同步和阻塞的概念前,需要知道
I/0 操作 主要分成兩部分 ① 數據准備,將數據加載到內核緩存(數據加載到操作系統) ② 將內核緩存中的數據加載到用戶緩存(從操作系統復制到應用中)
同步和異步的概念描述的是用戶線程與內核的交互方式
阻塞和非阻塞的概念描述的是用戶線程調用內核IO操作的方式
異步就是異步
同步和異步針對應用程序來,關注的是程序中間的協作關系;
阻塞與非阻塞更關注的是單個進程的執行狀態。
阻塞、非阻塞、多路IO復用,都是同步IO,異步必定是非阻塞的,所以不存在異步阻塞和異步非阻塞的說法。
真正的異步IO需要CPU的深度參與。換句話說,只有用戶線程在操作IO的時候根本不去考慮IO的執行全部都交給CPU去完成,
而自己只等待一個完成信號的時候,才是真正的異步IO。
所以,拉一個子線程去輪詢、去死循環,或者使用select、poll、epool,都不是異步。
PS:
1,異步是一個相對概念,實際應用中沒有絕對的異步,現實中更多稱為“異步”只是代表阻塞。
2,不同場合,語言環境,概念不一樣,有時候同步就代表了阻塞,異步表示非阻塞。如果細分的話,代表不同含義。
PS:【同步/異步】和【阻塞/非阻塞】的關注點是存在區別的:
【同步/異步】表示是兩個事件交互的是否有序依賴關系 同步:針對執行結果,A事件必須知道B事件的結果M后才執行得到結果。 異步:針對執行結果,執行A事件和執行B事件沒有關系。
阻塞/非阻塞表示執行過程出現的狀態 阻塞:針對執行者來說,執行A事件,執行過程因為條件未滿足,執行狀態變成等待狀態。 非阻塞:針對執行者來說,就是事件A執行遇到未滿足條件,執行另外獨立的C事件。
總結:兩者之間是沒有關系的 【同步/異步】
概念上是:事件A,B的結果之間的是否存在依賴關系;
影響上是:保證依賴數據的正確性 【阻塞/非阻塞】
概念上是:自身執行狀態。 影響上是:阻塞導致資源浪費。
特別注意:異步只有異步,同步才有阻塞和非阻塞的說法!
例子: 總整體看:傳統的請求,是同步的(也是阻塞的),請求響應是有序的(請求響應之間也是等待的);AJAX是異步請求(也是非阻塞的)。 同步不等於阻塞: 單個看:AJAX從客戶端執行單個請求看數據是同步,但是執行是非阻塞,在未收到響應繼續執行其他請求。
一、同步阻塞IO
同步阻塞IO模型是最簡單的IO模型,用戶線程在內核進行IO操作時被阻塞。
用戶線程通過系統調用read發起IO讀操作,由用戶空間轉到內核空間。
內核等到數據包到達后,然后將接收的數據拷貝到用戶空間,完成read操作。
二、同步非阻塞IO
同步非阻塞IO是在同步阻塞IO的基礎上,將socket設置為NIO(NONBLOCK)。
這樣做用戶線程可以在發起IO請求后可以立即返回。
由於socket是非阻塞的方式,因此用戶線程發起IO請求時立即返回。
但並未讀取到任何數據,用戶線程需要不斷地發起IO請求,直到數據到達后,才真正讀取到數據,繼續執行。
三、IO多路復用
IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,
使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。
用戶首先將需要進行IO操作的socket添加到select中,然后阻塞等待select系統調用返回。
當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據並繼續執行。
從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,
甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。
但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。
用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。
而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
然而,使用select函數的優點並不僅限於此。
雖然上述方式允許單線程內處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞IO模型還要長。
如果用戶線程只注冊自己感興趣的socket或者IO請求,然后去做自己的事情,等到數據到來時再進行處理,則可以提高CPU的利用率。
IO多路復用模型使用了Reactor設計模式實現了這一機制。
補充:IO多路復用又叫“事件驅動”
首先,要從你常用的IO操作談起,比如read和write,通常IO操作都是阻塞I/O的,也就是說當你調用read時,
如果沒有數據收到,那么線程或者進程就會被掛起,直到收到數據。 這樣,當服務器需要處理1000個連接的的時候,而且只有很少連接忙碌的,
那么會需要1000個線程或進程來處理1000個連接,而1000個線程大部分是被阻塞起來的。
由於CPU的核數或超線程數一般都不大,比如4,8,16,32,64,128,比如4個核要跑1000個線程,那么每個線程的時間槽非常短,而線程切換非常頻繁。
這樣是有問題的:
1,線程是有內存開銷的,1個線程可能需要512K(或2M)存放棧,那么1000個線程就要512M(或2G)內存。
2,線程的切換,或者說上下文切換是有CPU開銷的,當大量時間花在上下文切換的時候,分配給真正的操作的CPU就要少很多。 那么,我們就要引入非阻塞I/O的概念,非阻塞IO很簡單,通過fcntl(POSIX)或ioctl(Unix)設為非阻塞模式,
這時,當你調用read時,如果有數據收到,就返回數據,如果沒有數據收到,就立刻返回一個錯誤,如EWOULDBLOCK。
這樣是不會阻塞線程了,但是你還是要不斷的輪詢來讀取或寫入。 於是,我們需要引入IO多路復用的概念。
多路復用是指使用一個線程來檢查多個文件描述符(Socket)的就緒狀態,比如調用select和poll函數,傳入多個文件描述符,
如果有一個文件描述符就緒,則返回,否則阻塞直到超時。
得到就緒狀態后進行真正的操作可以在同一個線程里執行,也可以啟動線程執行(比如使用線程池)。 這樣在處理1000個連接時,只需要1個線程監控就緒狀態,對就緒的每個連接開一個線程處理就可以了,
這樣需要的線程數大大減少,減少了內存開銷和上下文切換的CPU開銷。
四、異步IO
“真正”的異步IO需要操作系統更強的支持。
在IO多路復用模型中,事件循環將文件句柄的狀態事件通知給用戶線程,由用戶線程自行讀取數據、處理數據。
而在異步IO模型中,當用戶線程收到通知時,數據已經被內核讀取完畢,並放在了用戶線程指定的緩沖區內,內核在IO完成后通知用戶線程直接使用即可。
異步IO模型使用了Proactor設計模式實現了這一機制。
相比於IO多路復用模型,異步IO並不十分常用,不少高性能並發服務程序使用IO多路復用模型+多線程任務處理的架構基本可以滿足需求。
況且目前操作系統對異步IO的支持並非特別完善,更多的是采用IO多路復用模型模擬異步IO的方式
(IO事件觸發時不直接通知用戶線程,而是將數據讀寫完畢后放到用戶指定的緩沖區中)。
Java7之后已經支持了異步IO,感興趣的讀者可以嘗試使用。