JAVA 中IO總結 同步、異步、阻塞、非阻塞

最近總結JAVA中的IO,遇到了有關阻塞、非阻塞、同步、異步的概念,以前也作個內核有關開發,今天溫故而知新。linux

Linux支持同步IO,也支持異步IO,所以分爲同步阻塞、同步非阻塞,異步阻塞,異步非阻塞。git

1、同步阻塞編程

這是早期Linux經常使用的IO方式,在這個模型中,用戶空間的應用程序執行一個系統調用,這會致使應用程序阻塞。這意味着應用程序會一直阻塞,直到系統調用完成爲止(數據傳輸完成或發生錯誤)。調用應用程序處於一種再也不消費 CPU 而只是簡單等待響應的狀態,所以從處理的角度來看,這是很是有效的。圖 1 給出了傳統的阻塞 I/O 模型,這也是目前應用程序中最爲經常使用的一種模型。其行爲很是容易理解,其用法對於典型的應用程序來講都很是有效。在調用 read 系統調用時,應用程序會阻塞並對內核進行上下文切換。而後會觸發讀操做,當響應返回時(從咱們正在從中讀取的設備中返回),數據就被移動到用戶空間的緩衝區中。而後應用程序就會解除阻塞(read 調用返回)。多線程

圖1 同步阻塞方式異步

 

2、同步非阻塞 I/O

 

同步阻塞 I/O 的一種效率稍低的變種是同步非阻塞 I/O。在這種模型中,設備是以非阻塞的形式打開的。這意味着 I/O 操做不會當即完成,read操做可能會返回一個錯誤代碼,說明這個命令不能當即知足(EAGAIN 或 EWOULDBLOCK),如圖 2 所示。socket

圖2 同步非阻塞方式
 

非阻塞的實現是 I/O 命令可能並不會當即知足,須要應用程序調用許屢次來等待操做完成。這可能效率不高,由於在不少狀況下,當內核執行這個命令時,應用程序必需要進行忙碌等待,直到數據可用爲止,或者試圖執行其餘工做。正如圖 3 所示的同樣,這個方法能夠引入 I/O 操做的延時,由於數據在內核中變爲可用到用戶調用 read 返回數據之間存在必定的間隔,這會致使總體數據吞吐量的下降。函數

3、異步阻塞方式性能

另一個阻塞解決方案是帶有阻塞通知的非阻塞 I/O。在這種模型中,配置的是非阻塞 I/O,而後使用阻塞 select 系統調用來肯定一個 I/O 描述符什麼時候有操做。使 select 調用很是有趣的是它能夠用來爲多個描述符提供通知,而不只僅爲一個描述符提供通知。對於每一個提示符來講,咱們能夠請求這個描述符能夠寫數據、有讀數據可用以及是否發生錯誤的通知。ui

圖3 異步阻塞方式線程

4、異步非阻塞方式

最後,異步非阻塞 I/O 模型是一種處理與 I/O 重疊進行的模型。讀請求會當即返回,說明 read 請求已經成功發起了。在後臺完成讀操做時,應用程序而後會執行其餘處理操做。當 read 的響應到達時,就會產生一個信號或執行一個基於線程的回調函數來完成此次 I/O 處理過程。


  

圖4 異步非阻塞方式

在一個進程中爲了執行多個 I/O 請求而對計算操做和 I/O 處理進行重疊處理的能力利用了處理速度與 I/O 速度之間的差別。當一個或多個 I/O 請求掛起時,CPU 能夠執行其餘任務;或者更爲常見的是,在發起其餘 I/O 的同時對已經完成的 I/O 進行操做。

5、同步與異步
同步/異步, 它們是消息的通知機制

1. 概念解釋
A. 同步
所謂同步,就是在發出一個功能調用時,在沒有獲得結果以前,該調用就不返回。

按照這個定義,其實絕大多數函數都是同步調用(例如sin isdigit等)。
可是通常而言,咱們在說同步、異步的時候,特指那些須要其餘部件協做或者須要必定時間完成的任務。
最多見的例子就是 SendMessage。
該函數發送一個消息給某個窗口,在對方處理完消息以前,這個函數不返回。
當對方處理完畢之後,該函數才把消息處理函數所返回的值返回給調用者。

B. 異步
異步的概念和同步相對。
當一個異步過程調用發出後,調用者不會馬上獲得結果。
實際處理這個調用的部件是在調用發出後,
經過狀態、通知來通知調用者,或經過回調函數處理這個調用。

以 Socket爲例,
當一個客戶端經過調用 Connect函數發出一個鏈接請求後,調用者線程不用等待結果,可馬上繼續向下運行。
當鏈接真正創建起來之後,socket底層會發送一個消息通知該對象。

C. 三種返回結果途徑 
執行部件和調用者能夠經過三種途徑返回結果:
a.   狀態、
b.   通知、
c.   回調函數。

可使用哪種依賴於執行部件的實現,除非執行部件提供多種選擇,不然不受調用者控制。

a. 若是執行部件用狀態來通知,
    那麼調用者就須要每隔必定時間檢查一次,效率就很低
    有些初學多線程編程的人,總喜歡用一個循環去檢查某個變量的值,這實際上是一種很嚴重的錯誤。

b. 若是是使用通知的方式,
    效率則很高,由於執行部件幾乎不須要作額外的操做。

c. 至於回調函數,
    和通知沒太多區別。


2. 舉例說明
理解這兩個概念,能夠用去銀行辦理業務(能夠取錢,也能夠存錢)來比喻:
當到銀行後,
.能夠去ATM機前排隊等候                                -- (排隊等候)就是同步等待消息
.能夠去大廳拿號,等到排到個人號時,
 櫃檯的人會通知我輪到我去辦理業務.              -- (等待別人通知)就是異步等待消息.

在異步消息通知機制中,
等待消息者(在這個例子中就是等待辦理業務的人)每每註冊一個回調機制,
在所等待的事件被觸發時由觸發機制(在這裏是櫃檯的人)經過某種機制(在這裏是寫在小紙條上的號碼)
找到等待該事件的人.

在select/poll 等IO 多路複用機制中就是fd,
當消息被觸發時,觸發機制經過fd 找處處理該fd的處理函數.

3. 在實際的程序中,
同步消息通知機制:就比如簡單的read/write 操做,它們須要等待這兩個操做成功才能返回;
                  同步, 是由處理消息者本身去等待消息是否被觸發;
異步消息通知機制:相似於select/poll 之類的多路複用IO 操做,
                  當所關注的消息被觸發時,由消息觸發機制通知觸發對消息的處理.
                  異步, 由觸發機制來通知處理消息者;


仍是回到上面的例子,
輪到你辦理業務, 這個就是你關注的消息,
而辦理什麼業務, 就是對這個消息的處理,
二者是有區別的.

而在真實的IO 操做時: 所關注的消息就是     該fd是否可讀寫,
                     而對消息的處理是     對這個fd 進行讀寫.

同步/異步僅僅關注的是如何通知消息,它們對如何處理消息並不關心,
比如說,銀行的人僅僅通知你輪到你辦理業務了,
而辦理業務什麼業務(存錢仍是取錢)他們是不知道的.

6、阻塞與非阻塞
阻塞/非阻塞, 它們是程序在等待消息(無所謂同步或者異步)時的狀態.

1. 概念解釋
A. 阻塞
阻塞調用是指調用結果返回以前,當前線程會被掛起。函數只有在獲得結果以後纔會返回。
有人也許會把阻塞調用和同步調用等同起來,實際上他是不一樣的。
對於同步調用來講,不少時候當前線程仍是激活的,只是從邏輯上當前函數沒有返回而已。

socket接收數據函數recv是一個阻塞調用的例子。
當socket工做在阻塞模式的時候, 若是沒有數據的狀況下調用該函數,則當前線程就會被掛起,直到有數據爲止。

B. 非阻塞
非阻塞和阻塞的概念相對應,指在不能馬上獲得結果以前,該函數不會阻塞當前線程,而會馬上返回。

C. 對象的阻塞模式和阻塞函數調用
對象是否處於阻塞模式和函數是否是阻塞調用有很強的相關性,可是並非一一對應的。


阻塞對象上能夠有非阻塞的調用方式,咱們能夠經過必定的API去輪詢狀態,
在適當的時候調用阻塞函數,就能夠避免阻塞。
而對於非阻塞對象,調用特殊的函數也能夠進入阻塞調用。函數select就是這樣的一個例子。

2. 舉例說明
繼續上面的那個例子,
不管是排隊等待,仍是使用號碼等待通知,
若是在這個等待的過程當中,
. 等待者除了等待消息以外不能作其它的事情,那麼該機制就是阻塞的,
  表如今程序中,也就是該程序一直阻塞在該函數調用處不能繼續往下執行.
. 相反,有的人喜歡在銀行辦理這些業務的時候一邊打打電話發發短信一邊等待,這樣的狀態就是非阻塞的,
  由於他(等待者)沒有阻塞在這個消息通知上,而是一邊作本身的事情一邊等待.

7、易混淆的點
不少人也會把異步和非阻塞混淆,
由於異步操做通常都不會在真正的IO 操做處被阻塞,
好比若是用select 函數,當select 返回可讀時再去read 通常都不會被阻塞
就比如當你的號碼排到時通常都是在你以前已經沒有人了,因此你再去櫃檯辦理業務就不會被阻塞.
可見,同步/異步與阻塞/非阻塞是兩組不一樣的概念,它們能夠共存組合,

而不少人之因此把同步和阻塞混淆,我想也是由於沒有區分這兩個概念,
好比阻塞的read/write 操做中,實際上是把消息通知和處理消息結合在了一塊兒,
在這裏所關注的消息就是fd 是否可讀/寫,而處理消息則是對fd 讀/寫.
當咱們將這個fd 設置爲非阻塞的時候,read/write 操做就不會在等待消息通知這裏阻塞,
若是fd 不可讀/寫則操做當即返回.


8、同步/異步與阻塞/非阻塞的組合分析
_______阻塞____________________非阻塞_____
同步 | 同步阻塞              同步非阻塞
異步 | 異步阻塞              異步非阻塞

同步阻塞形式:
  效率是最低的,
  拿上面的例子來講,就是你專心排隊,什麼別的事都不作。

  實際程序中
  就是未對fd 設置O_NONBLOCK 標誌位的read/write 操做,

異步阻塞形式:
  若是在銀行等待辦理業務的人採用的是異步的方式去等待消息被觸發,也就是領了一張小紙條,
  假如在這段時間裏他不能離開銀行作其它的事情,那麼很顯然,這我的被阻塞在了這個等待的操做上面;


  異步操做是能夠被阻塞住的,只不過它不是在處理消息時阻塞,而是在等待消息被觸發時被阻塞.
  好比select 函數,
  假如傳入的最後一個timeout 參數爲NULL,那麼若是所關注的事件沒有一個被觸發,
  程序就會一直阻塞在這個select 調用處.

同步非阻塞形式:
  其實是效率低下的,
  想象一下你一邊打着電話一邊還須要擡頭看到底隊伍排到你了沒有,
  若是把打電話和觀察排隊的位置當作是程序的兩個操做的話,
  這個程序須要在這兩種不一樣的行爲之間來回的切換,效率可想而知是低下的;

  不少人會寫阻塞的read/write 操做,
  可是別忘了能夠對fd 設置O_NONBLOCK 標誌位,這樣就能夠將同步操做變成非阻塞的了;

異步非阻塞形式:
  效率更高,
  由於打電話是你(等待者)的事情,而通知你則是櫃檯(消息觸發機制)的事情,
  程序沒有在兩種不一樣的操做中來回切換.

  好比說,這我的忽然發覺本身煙癮犯了,須要出去抽根菸,
  因而他告訴大堂經理說,排到我這個號碼的時候麻煩到外面通知我一下(註冊一個回調函數),
  那麼他就沒有被阻塞在這個等待的操做上面,天然這個就是異步+非阻塞的方式了.

  若是使用異步非阻塞的狀況,
  好比aio_*組的操做,當發起一個aio_read 操做時,函數會立刻返回不會被阻塞,
  當所關注的事件被觸發時會調用以前註冊的回調函數進行處理.

 

9、epoll模型:

一、select是幾乎全部unix、linux都支持的一種多路IO方式,經過select函數發出IO請求後,線程阻塞,一直到數據準備完畢,而後才能把數據從核心空間拷貝到用戶空間,因此select是同步阻塞方式。int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

二、poll對select的使用方法進行了一些改進,突破了最大文件數的限制,同時使用更加方便一些。

int poll(struct pollfd *ufds, unsigned int nfds, int timeout);

struct pollfd {

   int fd;           /* 對應的文件描述符 */

   short events;     /* 要監聽的事件,例如POLLIN|POLLPRI */

  short revents;    /* 返回的事件,用於在poll返回時攜帶該fd上發生的事情,在poll調用時,該字段會自動被清空 */};

經過poll函數發出IO請求後,線程阻塞,直到數據準備完畢,poll函數在pollfd中經過revents字段返回事件,而後線程把數據從核心空間拷貝到用戶空間,因此poll一樣是同步阻塞方式,性能同select相比沒有改進。

三、epoll是linux爲了解決select/poll的性能問題而新搞出來的機制,基本的思路是:由專門的內核線程來不停地掃描fd列表,有結果後,把結果放到fd相關的鏈表中,用戶線程只須要按期從該fd對應的鏈表中讀取事件就能夠了。同時,爲了節省把數據從核心空間拷貝到用戶空間的消耗,採用了mmap的方式,容許程序在用戶空間直接訪問數據所在的內核空間,不須要把數據copy一份。epoll一共有3個函數:

 

 

      1.建立epoll文件描述符

 

      int epoll_create(int size);



      2.把須要監聽的文件fd和事件加入到epoll文件描述符,也能夠對已有的fd進行修改和刪除

 

      文件fd保存在一個紅黑樹中,該fd的事件保存在一個鏈表中(每一個fd一個事件鏈表),事件由內核線程負責填充,用戶線程讀取

 

      int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

 

                  typedef union epoll_data {

 

                      void *ptr;

 

                      int fd;

 

                      __uint32_t u32;

 

                      __uint64_t u64;

 

                  } epoll_data_t;




                  struct epoll_event {

 

                      __uint32_t events;      /* Epoll events */

 

                      epoll_data_t data;      /* User data variable */

 

                  };



      用戶線程按期輪詢epoll文件描述符上的事件,事件發生後,讀取事件對應的epoll_data,該結構中包含了文件fd和數據地址,因爲採用了mmap,程序能夠直接讀取數據。

 

      int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);



      有人把epoll這種方式叫作同步非阻塞(NIO),由於用戶線程須要不停地輪詢,本身讀取數據,看上去好像只有一個線程在作事情

 

      也有人把這種方式叫作異步非阻塞(AIO),由於畢竟是內核線程負責掃描fd列表,並填充事件鏈表的

 

      我的認爲真正理想的異步非阻塞,應該是內核線程填充事件鏈表後,主動通知用戶線程,或者調用應用程序事先註冊的回調函數來處理數據,若是還須要用戶線程不停的輪詢來獲取事件信息,就不是太完美了,因此也有很多人認爲epoll是僞AIO,仍是有道理的。
相關文章
相關標籤/搜索