聊聊同步、異步、阻塞、非阻塞以及IO模型


前言

在使用Netty改造手寫RPC框架的時候,需要給大家介紹一些相關的知識,這樣很多東西大家就可以看明白了,手寫RPC是一個支線任務,后續重點仍然是Kubernetes相關內容。

阻塞與非阻塞 同步與異步

阻塞與非阻塞

阻塞和非阻塞是進程在訪問數據的時候,數據是否准備就緒的一種處理方式。當數據沒有准備的時候,阻塞需要等待調用結果返回之前,進程會被掛起,函數只有在得到結果之后才會返回。非阻塞和阻塞的概念相對,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。

同步與異步

同步指的是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回。也就是必須一件一件事做,等前一件做完了才能做下一件事。異步的概念和同步相對,當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的部件在完成后,通過狀態、通知和回調來通知調用者。

舉個例子來說,對於我們經常使用B/S架構來說,同步和異步指的是從客戶端發起訪問數據的請求,阻塞和非阻塞指的是服務端進程訪問數據,進程是否需要等待。這兩者存在本質的區別,它們的修飾對象是不同的。

阻塞和非阻塞是指進程訪問的數據如果尚未就緒,進程是否需要等待,簡單說這相當於函數內部的實現區別,也就是未就緒時是直接返回還是等待就緒。

同步和異步是指訪問數據的機制,同步一般指主動請求並等待I/O操作完畢的方式,當數據就緒后在讀寫的時候必須阻塞,異步則指主動請求數據后便可以繼續處理其它任務,隨后等待I/O,操作完畢的通知,這可以使進程在數據讀寫時也不阻塞。

舉個例子

媽媽讓我去廚房燒一鍋水,准備下餃子。

阻塞:水只要沒燒開,我就干瞪眼看着這個鍋,滄海桑田,日新月異,我自巋然不動,廚房就是我的家,燒水是我的宿命;

非阻塞:我先去我屋子里打把王者,但是每過一分鍾,我都要去廚房瞅一眼,生怕時間長了,水燒干了就壞了,這樣導致我游戲也心思打,果不然,又掉段了;

同步:不管是每分鍾過來看一眼鍋,還是寸步不離的一直看着鍋,只要我不去看,我就不知道水燒好沒有,浪費時間啊,一寸光陰一寸金;

異步:我在淘寶買了一個電水壺,只要水開了,它就發出響聲,嗨呀,可以安心打王者嘍,打完可以吃餃子嘍;

總結:

阻塞/非阻塞:我在等你干活的時候我在干啥?

阻塞:啥也不干,死等;

非阻塞:可以干別的,但也要時不時問問你的進度;

同步/異步:你干完了,怎么讓我知道呢?

同步:我只要不問,你就不告訴我;

異步:你干完了,直接喊我過來就行;

IO模型

網絡IO的本質是Socket的讀取,Socket在Linux系統被抽象為流,IO可以理解為對流的操作。Linux標准文件訪問方式如下:

img
img

當發起一個read操作的時候,會經歷2個階段:

  1. 等待數據准備;
  2. 將數據從內核拷貝到進程中;

對於socket流也會經歷兩個階段:

  1. 將磁盤或者其他設備到達以后的信息,拷貝到內核的緩存區中;
  2. 將內核的緩存區的數據復制到應用進程緩存中;

網絡應用需要處理的無非就是兩大類問題,網絡IO,數據計算。相對於后者,網絡IO的延遲,給應用帶來的性能瓶頸大於后者,接下來我們介紹下IO模型:

同步阻塞IO(blocking IO)

同步阻塞 IO 模型是最常用的一個模型,也是最簡單的模型。在Linux中,默認情況下所有的socket都是blocking。阻塞就是進程休息, CPU處理其它進程去了。

用戶空間的應用程序執行一個系統調用(recvform),這會導致應用程序阻塞,直到數據准備好,並且將數據從內核復制到用戶進程,最后進程再處理數據,在等待數據到處理數據的兩個階段,整個進程都被阻塞。不能處理別的網絡IO。調用應用程序處於一種不再消費 CPU 而只是簡單等待響應的狀態,因此從處理的角度來看,這是非常有效的。在調用recv()/recvfrom()函數時,發生在內核中等待數據和復制數據的過程,大致如下圖:

img
img
  1. 應用進程向內核發起recfrom讀取數據;

  2. 准備數據報(應用進程阻塞);

  3. 將數據從內核負責到應用空間;

  4. 復制完成后,返回成功提示;

特點

同步阻塞 IO 整個過程都是阻塞的,對於用戶可以及時返回數據,無延遲,對於開發者來說簡單省事,對於系統來說無法應對高並發訪問,以及用戶在等待期間也無法進行其他任何操作。

同步非阻塞IO(nonblocking IO)

同步非阻塞就是采用輪詢的方式,定時去查看數據是否准備完成。在這種模型中,進程是以非阻塞的形式打開的。IO 操作不會立即完成,如果該緩沖區沒有數據的話,就會直接返回一個EWOULDBLOCK錯誤,不會讓應用一直等待中。

非阻塞IO也會進行recvform系統調用,檢查數據是否准備好,與阻塞IO不一樣,非阻塞將大的整片時間的阻塞分成N多的小的阻塞, 所以進程不斷地有機會被CPU訪問。也就是說非阻塞的recvform系統調用調用之后,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒准備好,此時會返回一個error。

img
img
  1. 應用進程向內核發起recvfrom讀取數據;

  2. 沒有數據報准備好,即刻返回EWOULDBLOCK錯誤碼;

  3. 應用進程向內核發起recvfrom讀取數據;

  4. 已有數據包准備好就進行一下步驟,否則還是返回錯誤碼;

  5. 將數據從內核拷貝到用戶空間;

  6. 完成后,返回成功提示;

特點

同步非阻塞方式相比同步阻塞方式,在等待任務期間進程可以處理其他事情,缺點的話就是因為采用定時輪詢的方式,導致系統整體的吞吐量降低。

IO多路復用( IO multiplexing)

同步非阻塞方式需要不斷主動輪詢,輪詢占據了很大一部分過程,輪詢會消耗大量的CPU時間,當並發情況下服務器很可能一瞬間會收到幾十上百萬的請求,這種情況下同步非阻塞IO需要創建幾十上百萬的線程去讀取數據,同時又因為應用線程是不知道什么時候會有數據讀取,為了保證消息能及時讀取到,那么這些線程自己必須不斷的向內核發送recvfrom 請求來讀取數據。這么多的線程不斷調用recvfrom 請求數據,明細是對線程資源的浪費。

於是有人就想到了由一個線程循環查詢多個任務的完成狀態(fd文件描述符),只要有任何一個任務完成,就去處理它。這樣就可以只需要一個或幾個線程就可以完成數據狀態詢問的操作,當有數據准備就緒之后再分配對應的線程去讀取數據,這么做就可以節省出大量的線程資源出來,這個就是IO多路復用。

img
img
  1. 應用進程向內核發起select調用;

  2. kernel會監聽所有select負責的socket;

  3. 任何一個socket中的數據准備好了,select就會返回;

  4. 應用進程再調用recvfrom操作,將數據從內核拷貝到用戶空間;

  5. 完成后,返回成功提示;

特點

IO多路復用與同步非阻塞相比,應用線程通過調select/poll之后,阻塞住,進入到內核態后由內核線程來輪詢這個應用線程所關注的所有文件描述符對應的緩沖區是否有數據准備就緒,只要有一個緩沖區數據准備就緒,就可以進行數據拷貝然后返回給用戶線程,這種方式就減少了用戶線程的不斷輪詢以及避免在每次輪詢時所產生的兩次上下文切換過程。

此外就是IO多路復用模型可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時(這里並不是全部數據可讀或可寫),才真正調用I/O操作函數。

此外還需要注意的是,IO多路復用既然可以處理多個IO,也就帶來了新的問題,多個IO之間的順序變得不確定了。

信號驅動IO(signal-driven IO)

IO多路復用解決了一個線程或者多個線程可以監控多個文件描述符的問題,但是select是采用輪詢的方式來監控多個文件描述符的,通過不斷的輪詢文件描述符的可讀狀態來知道是否有可讀的數據,這樣無腦的輪詢就顯得有點浪費,因為大部分情況下的輪詢都是無效的,於是乎有人就想,能不能不要總是去輪詢數據是否准備就緒,能不能發出請求后,等數據准備好了在通知我,所以這樣就出現了信號驅動IO。

信號驅動IO不是用循環請求詢問的方式去監控數據就緒狀態,而是在調用sigaction時候建立一個SIGIO的信號聯系,當內核數據准備好之后再通過SIGIO信號,通知線程數據准備好后的可讀狀態,當線程收到可讀狀態的信號后,此時再向內核發起recvfrom讀取數據的請求。因為信號驅動IO的模型下,應用線程在發出信號監控后即可返回,不會阻塞,所以這樣的方式下,一個應用線程也可以同時監控多個文件描述符。

image.png
image.png
  1. 應用進程開啟套接口信號驅動IO功能,通過系統調用sigaction執行一個信號處理函數,請求即刻返回;

  2. 當數據准備就緒時,就生成對應進程的SIGIO信號,通過信號回調通知應用進程;

  3. 應用進程再調用recvfrom操作,將數據從內核拷貝到用戶空間;

  4. 完成后,返回成功提示;

特點

信號驅動IO相比於IO多路復用,在通過這種建立信號關聯的方式,實現了發出請求后只需要等待數據就緒的通知即可,這樣就可以避免大量無效的數據狀態輪詢操作。

異步非阻塞 IO(asynchronous IO)

不管是IO多路復用還是信號驅動,我們要讀取數據的時候,總是要發起兩階段的請求,第一次發送select請求,詢問數據狀態是否准備好,第二次發送recevform請求讀取數據。這個時候我們會有一個疑問,為什么在讀數據之前總要有個數據就緒的狀態,可不可以應用進程只需要向內核發送一個read 請求,告訴內核要讀取數據后,就立即返回。當內核數據准備就緒,內核會主動把數據從內核復制到用戶空間,等所有操作都完成之后,內核會發起一個通知告訴應用,所以這樣就出現了異步非阻塞 IO模型。

異步非阻塞IO模型應用進程發起aio_read操作之后,立刻就可以開始去做其它的事。后續的操作有內核接管,當內核收到一個asynchronous read之后,它會立刻返回,不會對用戶進程產生任何block。然后,內核會等待數據准備完成,然后將數據拷貝到用戶內存,當這一切都完成以后,內核會給用戶進程發送一個signal或執行一個基於線程的回調函數來完成這次 IO 處理過程。

image.png
image.png
  1. 應用進程發起aio_read操作,立即返回;

  2. 內核等待數據准備完成,然后將數據拷貝到用戶內存;

  3. 內核會給用戶進程發送一個signal信號;

  4. 收到信號,返回成功提示;

特點

異步非阻塞 IO相比於信號驅動IO,信號驅動IO模型只是由內核通知我們可以開始下一個IO操作,而異步非阻塞 IO模型是由內核通知我們操作什么時候完成。

五種IO模型總結

阻塞IO和非阻塞IO區別

調用阻塞IO會一直阻塞住對應的進程直到操作完成,而非阻塞IO在內核還准備數據的情況下會立刻返回。

同步IO和異步IO區別

兩者的區別就在於同步IO做IO操作的時候會將進程阻塞,也就是應用進程調用recvfrom操作,recvfrom會將數據從內核拷貝到用戶內存中,在這段時間內,進程是被阻塞的。

img
img

舉個例子

小王去買火車票,三天后買到一張退票。參演人員(老李,黃牛,售票員,快遞員),往返車站耗費1小時。

同步阻塞 IO

小王去火車站買票,排隊三天買到一張退票。整個三天小王無法做其他事情,只能做買票的一件事情。

同步非阻塞 IO

小王去火車站買票,隔一天去火車站問有沒有退票,三天后買到一張票。整個過程中小王需要往返3次,往返消耗3小時,這個期間小王可以做其他事情。

IO多路復用
select/poll

小王去火車站買票,委托黃牛購買,然后每隔12小時打電話詢問黃牛,黃牛三天買到票,然后小王去火車站交錢領票。整個小王需要往返2次,往返消耗2小時,黃牛需要手續費100,打電話6次,這里的黃牛就是select/poll,多路指的就是一個黃牛可以服務多個人。

epoll

小王去火車站買票,委托黃牛購買,黃牛買到后即通知小王去領,然后小王去火車站交錢領票。整個過程小王需要往返2次,往返消耗2小時,黃牛需要手續費100,無需打電話。

信號驅動IO

小王去火車站買票,售票員留下電話,有票后,售票員電話通知小王,然后小王去火車站交錢領票。整個過程小王需要往返2次,往返消耗2小時,無手續費,無需打電話。

異步非阻塞 IO

小王去火車站買票,給售票員留下電話,有票后,售票員電話通知小王並快遞送票上門。整個過程小王需要往返1次,往返消耗1小時,無手續費,無需打電話。

參考

IO 多路復用是什么意思

100%弄明白5種IO模型

聊聊Linux 五種IO模型

結束

歡迎大家點點關注,點點贊!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM