天天看點

【轉載】聊聊同步、異步、阻塞與非阻塞

本文轉自 https://www.jianshu.com/p/aed6067eeac9

關于IO多路複用,網上有各種說法,最常見的是認為是異步阻塞的,比如這裡的https://cloud.tencent.com/developer/article/1165632,我對這個觀點不是很認同,這個更像是對IO多路複用模型,即各種架構如ACE中的reactor,nodejs的實作描述,而不是對系統層面IO多路複用的描述;

還有聊聊Linux 五種IO模型中詳細講解了IO模型,寫的很不錯, https://www.jianshu.com/p/486b0965c296,作者将IO多路複用歸為同步阻塞,理由是select時是阻塞的,我部分認同這個觀點,,我比較認可

https://github.com/CyC2018/CS-Notes/issues/194 這裡的觀點,即IO多路複用是同步的,阻塞或者不阻塞

【轉載】聊聊同步、異步、阻塞與非阻塞

近來遇到了一些常見的概念,尤其是網絡程式設計方面的概念,如:阻塞、非阻塞、異步I/O等等,對于這些概念自己也沒有太清晰的認識,隻是很模糊的概念,說了解吧也了解,但是要讓自己準确的描述概念方面的具體細節,卻說的不那麼準确,這也是自己在這幾個方面也沒有細細考究過的原因吧。經過看了些這幾個概念的資料,發現同步、異步、阻塞、非阻塞的概念其實也并不難以了解,在此寫下此文,歡迎拍磚,希望多多交流。

<code>首先來解釋同步和異步的概念,這兩個概念與消息的通知機制有關。也就是同步與異步主要是從消息通知機制角度來說的。</code>

<code>所謂同步就是一個任務的完成需要依賴另外一個任務時,隻有等待被依賴的任務完成後,依賴的任務才能算完成,這是一種可靠的任務序列</code>。要麼成功都成功,失敗都失敗,兩個任務的狀态可以保持一緻。

<code>所謂異步是不需要等待被依賴的任務完成,隻是通知被依賴的任務要完成什麼工作,依賴的任務也立即執行,隻要自己完成了整個任務就算完成了</code>。至于被依賴的任務最終是否真正完成,依賴它的任務無法确定,<code>是以它是不可靠的任務序列</code>。

<code>異步的概念和同步相對</code>。當一個同步調用發出後,<code>調用者要一直等待傳回消息(結果)通知後</code>,才能進行後續的執行;當一個異步過程調用發出後,調用者不能立刻得到傳回消息(結果)。<code>實際處理這個調用的部件在完成後,通過狀态、通知和回調來通知調用者</code>。

這裡提到執行部件和調用者通過三種途徑傳回結果:<code>狀态、通知和回調</code>。使用哪一種通知機制,<code>依賴于執行部件的實作</code>,除非執行部件提供多種選擇,<code>否則不受調用者控制</code>。

如果執行部件用狀态來通知,那麼調用者就需要每隔一定時間檢查一次,效率就很低(有些初學多線程程式設計的人,總喜歡用一個循環去檢查某個變量的值,這其實是一種很嚴重的錯誤); 如果是使用通知的方式,效率則很高,因為執行部件幾乎不需要做額外的操作。至于回調函數,其實和通知沒太多差別。

舉個例子,比如我去銀行辦理業務,可能會有兩種方式:

選擇排隊等候; 另種選擇取一個小紙條上面有我的号碼,等到排到我這一号時由櫃台的人通知我輪到我去辦理業務了;

第一種:<code>前者(排隊等候)就是同步等待消息通知</code>,也就是我要一直在等待銀行辦理業務情況;

第二種:<code>後者(等待别人通知)就是異步等待消息通知</code>。在異步消息進行中,<code>等待消息通知者(在這個例子中就是等待辦理業務的人)往往注冊一個回調機制</code>,在所等待的事件被觸發時由觸發機制(在這裡是櫃台的人)通過某種機制(在這裡是寫在小紙條上的号碼,喊号)找到等待該事件的人。

<code>阻塞和非阻塞這兩個概念與程式(線程)等待消息通知(無所謂同步或者異步)時的狀态有關。也就是說阻塞與非阻塞主要是程式(線程)等待消息通知時的狀态角度來說的。</code>

<code>阻塞調用是指調用結果傳回之前,目前線程會被挂起,一直處于等待消息通知,不能夠執行其他業務</code>。函數隻有在得到結果之後才會傳回。

有人也許會把阻塞調用和同步調用等同起來,實際上它們是不同的。

<code>對于同步調用來說,很多時候目前線程可能還是激活的,隻是從邏輯上目前函數沒有傳回而已,此時,這個線程可能也會處理其他的消息</code>。還有一點,在這裡先擴充下:

(a) 如果這個線程在等待目前函數傳回時,仍在執行其他消息處理,那這種情況就叫做同步非阻塞; (b) 如果這個線程在等待目前函數傳回時,沒有執行其他消息處理,而是處于挂起等待狀态,那這種情況就叫做同步阻塞;

<code>是以同步的實作方式會有兩種:同步阻塞、同步非阻塞;同理,異步也會有兩種實作:異步阻塞、異步非阻塞;</code>

對于阻塞調用來說,則目前線程就會被挂起等待目前函數傳回;

非阻塞和阻塞的概念相對應,<code>指在不能立刻得到結果之前,該函數不會阻塞目前線程,而會立刻傳回</code>。雖然表面上看非阻塞的方式可以明顯的提高CPU的使用率,<code>但是也帶了另外一種後果就是系統的線程切換增加</code>。<code>增加的CPU執行時間能不能補償系統的切換成本需要好好評估</code>。

繼續上面的那個例子,不論是排隊還是使用号碼等待通知,<code>如果在這個等待的過程中,等待者除了等待消息通知之外不能做其它的事情,那麼該機制就是阻塞的</code>,表現在程式中,也就是該程式一直阻塞在該函數調用處不能繼續往下執行。

相反,<code>有的人喜歡在銀行辦理這些業務的時候一邊打打電話發發短信一邊等待,這樣的狀态就是非阻塞的</code>,因為他(等待者)沒有阻塞在這個消息通知上,而是一邊做自己的事情一邊等待。

但是需要注意了,<code>同步非阻塞形式實際上是效率低下的</code>,想象一下你一邊打着電話一邊還需要擡頭看到底隊伍排到你了沒有。如果把打電話和觀察排隊的位置看成是程式的兩個操作的話,這個程式需要在這兩種不同的行為之間來回的切換,效率可想而知是低下的;而<code>異步非阻塞形式卻沒有這樣的問題</code>,因為打電話是你(等待者)的事情,而通知你則是櫃台(消息觸發機制)的事情,程式沒有在兩種不同的操作中來回切換。

同步阻塞形式

效率是最低的,

拿上面的例子來說,就是你專心排隊,什麼别的事都不做。

實際程式中:就是未對fd 設定O_NONBLOCK标志位的read/write 操作;

異步阻塞形式

如果在銀行等待辦理業務的人<code>采用的是異步的方式去等待消息被觸發(通知)</code>,也就是領了一張小紙條,假如在這段時間裡他不能離開銀行做其它的事情,那麼很顯然,這個人被阻塞在了這個等待的操作上面;

<code>異步操作是可以被阻塞住的,隻不過它不是在處理消息時阻塞,而是在等待消息通知時被阻塞。</code>

比如select 函數,假如傳入的最後一個timeout參數為NULL,<code>那麼如果所關注的事件沒有一個被觸發,程式就會一直阻塞在這個select 調用處</code>。

同步非阻塞形式

實際上是效率低下的,

想象一下你一邊打着電話一邊還需要擡頭看到底隊伍排到你了沒有,如果把打電話和觀察排隊的位置看成是程式的兩個操作的話,<code>這個程式需要在這兩種不同的行為之間來回的切換</code>,效率可想而知是低下的。

很多人會寫阻塞的read/write 操作,<code>但是别忘了可以對fd設定O_NONBLOCK 标志位,這樣就可以将同步操作變成非阻塞的了</code>。

異步非阻塞形式

效率更高,

因為打電話是你(等待者)的事情,而通知你則是櫃台(消息觸發機制)的事情,<code>程式沒有在兩種不同的操作中來回切換</code>。

比如說,這個人突然發覺自己煙瘾犯了,需要出去抽根煙,于是他告訴大堂經理說,排到我這個号碼的時候麻煩到外面通知我一下(注冊一個回調函數),那麼他就沒有被阻塞在這個等待的操作上面,自然這個就是異步+非阻塞的方式了。

如果使用異步非阻塞的情況,比如aio_*組的操作,當發起一個aio_read操作時,<code>函數會馬上傳回不會被阻塞,當所關注的事件被觸發時會調用之前注冊的回調函數進行處理</code>。

很多人會把同步和阻塞混淆,我想是<code>因為很多時候同步操作會以阻塞的形式表現出來</code>,比如很多人會寫阻塞的read/write操作,但是别忘了可以對fd設定O_NONBLOCK标志位,這樣就可以将同步操作變成非阻塞的了。<code>但最根本是因為沒有區分這兩個概念</code>,比如阻塞的read/write操作中,<code>其實是把消息通知機制和等待消息通知的狀态結合在了一起</code>,在這裡<code>所關注的消息就是fd是否可讀/寫</code>,而<code>等待消息通知的狀态則是對fd可讀/寫等待過程中程式(線程)的狀态</code>。當我們将這個fd設定為非阻塞的時候,read/write操作就不會在等待消息通知這裡阻塞,如果fd不可讀/寫則操作立即傳回。

同樣的,很多人也會把異步和非阻塞混淆,<code>因為異步操作一般都不會在真正的IO操作處被阻塞</code>,比如如果用select函數,<code>當select傳回可讀時再去read一般都不會被阻塞,而是在select函數調用處阻塞</code>。

對上面所講的概念再次進行一個場景梳理,上面已經明确說明,<code>同步/異步關注的是消息通知的機制,而阻塞/非阻塞關注的是程式(線程)等待消息通知時的狀态</code>。以小明下載下傳檔案打個比方,從這兩個關注點來再次說明這兩組概念,希望能夠更好的促進大家的了解。

同步阻塞:小明一直盯着下載下傳進度條,到 100% 的時候就完成。

同步展現在:等待下載下傳完成通知; 阻塞展現在:等待下載下傳完成通知過程中,不能做其他任務處理;

同步非阻塞:小明送出下載下傳任務後就去幹别的,每過一段時間就去瞄一眼進度條,看到 100% 就完成。

非阻塞展現在:等待下載下傳完成通知過程中,去幹别的任務了,隻是時不時會瞄一眼進度條;【小明必須要在兩個任務間切換,關注下載下傳進度】

異步阻塞:小明換了個有下載下傳完成通知功能的軟體,下載下傳完成就“叮”一聲。不過小明仍然一直等待“叮”的聲音(看起來很傻,不是嗎)。

異步展現在:下載下傳完成“叮”一聲通知; 阻塞展現在:等待下載下傳完成“叮”一聲通知過程中,不能做其他任務處理;

異步非阻塞:仍然是那個會“叮”一聲的下載下傳軟體,小明送出下載下傳任務後就去幹别的,聽到“叮”的一聲就知道完成了。

非阻塞展現在:等待下載下傳完成“叮”一聲通知過程中,去幹别的任務了,隻需要接收“叮”聲通知即可;【軟體處理下載下傳任務,小明處理其他任務,不需關注進度,隻需接收軟體“叮”聲通知,即可】

也就是說,<code>同步/異步是“下載下傳完成消息”通知的方式(機制),而阻塞/非阻塞則是在等待“下載下傳完成消息”通知過程中的狀态(能不能幹其他任務)</code>,在不同的場景下,同步/異步、阻塞/非阻塞的四種組合都有應用。

是以,綜上所述,<code>同步和異步僅僅是關注的消息如何通知的機制,而阻塞與非阻塞關注的是等待消息通知時的狀态</code>。也就是說,<code>同步的情況下,是由處理消息者自己去等待消息是否被觸發,而異步的情況下是由觸發機制來通知處理消息者</code>,是以在異步機制中,<code>處理消息者和觸發機制之間就需要一個連接配接的橋梁</code>:

在銀行的例子中,這個橋梁就是小紙條上面的号碼。 在小明的例子中,這個橋梁就是軟體“叮”的聲音。

<code>最後,請大家注意了解“消息通知機制”和“等待消息通知時的狀态”這兩個概念,這是了解四個概念的關鍵所在。</code>