徹底搞懂同步非同步與阻塞非阻塞

蟬沐風發表於2023-02-15

上兩篇文章講過了BIO與非阻塞IO以及IO多路複用,洋洋灑灑近3萬字。

這篇文章我們來聊一個很簡單,但是很多人往往分不清的一個問題,同步非同步、阻塞非阻塞到底怎麼區分?

開篇先問大家一個問題:IO多路複用是同步IO還是非同步IO

先思考一下,再繼續往下讀。


鉅著《Unix網路程式設計》將IO模型劃分為5種,分別是

  • 阻塞IO
  • 非阻塞IO
  • IO複用
  • 訊號驅動IO
  • 非同步IO

個人認為這麼分類並不是很好,因為從字面上理解阻塞IO和阻塞IO就已經是數學意義上的全集了,怎麼又冒出了後邊3種模型,會給初學者帶來一些困擾。

接下來進入正文。

作者:蟬沐風,
公眾號:蟬沐風的碼場

1. 一個簡單的IO流程

讓我們先摒棄我們原本熟知的各種IO模型流程圖,先看一個非常簡單的IO流程,不涉及任何阻塞非阻塞、同步非同步概念的圖。

IO流程

客戶端發起系統呼叫之後,核心的操作可以被分成兩步:

  • 等待資料

    此階段網路資料進入網路卡,然後網路卡將資料放到指定的記憶體位置,此過程CPU無感知。然後經過網路卡發起硬中斷,再經過軟中斷,核心執行緒將資料傳送到socket的核心緩衝區中。

  • 資料複製

    資料從socket的核心緩衝區複製到使用者空間

2. 阻塞與非阻塞

阻塞與非阻塞在API上區別在於socket是否設定了SOCK_NONBLOCK這個引數,預設情況下是阻塞的,設定了該引數則為非阻塞。

2.1 阻塞

假設socket為阻塞模式,則IO呼叫如下圖所示。

阻塞示意圖

當處於執行狀態的使用者執行緒發起recv系統呼叫時,如果socket核心緩衝區內沒有資料,則核心會將當前執行緒投入睡眠,讓出CPU的佔用。

直到網路資料到達網路卡,網路卡DMA資料到記憶體,再經過硬中斷、軟中斷,由核心執行緒喚醒使用者執行緒。

此時socket的資料已經準備就緒,使用者執行緒由使用者態進入到核心態,執行資料複製,將資料從核心空間複製到使用者空間,系統呼叫結束。此階段,開發者通常認為使用者執行緒處於等待(稱為阻塞也行)狀態,因為在使用者態的角度上,執行緒確實啥也沒幹(雖然在核心態幹得累死累活)。

2.2 非阻塞

如果將socket設定為非阻塞模式,呼叫便換了一副光景。

非阻塞示意圖

使用者執行緒發起系統呼叫,如果socket核心緩衝區中沒有資料,則系統呼叫立即返回,不會掛起執行緒。而執行緒會繼續輪詢,直到socket核心緩衝區內有資料為止。

如果socket核心緩衝區內有資料,則使用者執行緒進入核心態,將資料從核心空間複製到使用者空間,這一步和2.1小節沒有區別。

3. 同步與非同步

同步非同步主要看請求發起方對訊息結果的獲取方式,是主動獲取還是被動通知。區別主要體現在資料複製階段。

3.1 同步

同步我們其實已經見識過了,2.1節和2.2節中的資料複製階段其實都是同步!

注:把同步的流程畫在阻塞和非阻塞的第二階段,並不是說阻塞和非阻塞的第二階段只能搭配同步手段!

同步指的是資料到達socket核心緩衝區之後,由使用者執行緒參與到資料複製過程中,直到資料從核心空間複製到使用者空間。

因此,IO多路複用,對於應用程式而言,仍然只能算是一種同步,因為應用程式仍然花費時間等待IO結果,等待期間CPU要麼用於遍歷檔案描述符的狀態,要麼用於休眠等待事件發生。

select為例,使用者執行緒發起select呼叫,會切換到核心空間,如果沒有資料準備就緒,則使用者執行緒阻塞到有資料來為止,select呼叫結束。結束之後使用者執行緒獲取到的只是「核心中有N個socket已經就緒」的這麼一個資訊,還需要使用者執行緒對著1024長度的描述符陣列進行遍歷,才能獲取到socket中的資料,這就是同步。

舉個生活中的例子,我們給物流客服打電話詢問我們的包裹是否已到達,如果未到達,我們就先睡一會兒,等到了之後客服給我們打電話把我們喊起來,然後我們屁顛屁顛地去快遞驛站拿快遞。這就是同步阻塞。

如果我們不想睡,就一直打電話問,直到包裹到了為止,然後再屁顛屁顛地去快遞驛站拿快遞。這就是同步非阻塞。

問題就是,能不能直接讓物流的人把快遞直接送到我家,別讓我自己去拿啊!這就是非同步。

3.2 理想的非同步

我們理想中的完美非同步應該是使用者程式發起非阻塞呼叫,核心直接返回結果之後,使用者執行緒可以立即處理下一個任務,只需要IO完成之後透過訊號或回撥函式的方式將資料傳遞給使用者執行緒。如下圖所示。

理想的非同步IO

因此,在理想的非同步環境下,資料準備階段和資料複製階段都是由核心完成的,不會對使用者執行緒進行阻塞,這種核心級別的改進自然需要作業系統底層的功能支援。

3.3 現實的非同步

現實比理想要骨感一些。

Linux核心並沒有太惹眼的非同步IO機制,這難不倒各路大神,比如Node的作者採用多執行緒模擬了這種非同步效果。

比如讓某個主執行緒執行主要的非IO邏輯操作,另外再起多個專門用於IO操作的執行緒,讓IO執行緒進行阻塞IO或者非阻塞IO加輪詢的方式來完成資料獲取,透過IO執行緒和主執行緒之間通訊進行資料傳遞,以此來實現非同步。

多執行緒模擬非同步

還有一種方案是Windows上的IOCP,它在某種程度上提供了理想的非同步,其內部依然採用的是多執行緒的原理,不過是核心級別的多執行緒。

遺憾的是,用Windows做伺服器的專案並不是特別多,期待Linux在非同步的領域上取得更大的進步吧。

4. 非同步阻塞?

說完了同步非同步、阻塞非阻塞,一個很自然的操作就是對他們進行排列組合。

  • 同步阻塞
  • 同步非阻塞
  • 非同步非阻塞
  • 非同步阻塞

但是非同步阻塞是什麼鬼?按照上文的解釋,該IO模型在第一階段應該是使用者執行緒阻塞,等待資料;第二階段應該是核心執行緒(或專門的IO執行緒)處理IO操作,然後把資料透過事件或者回撥的方式通知使用者執行緒,既然如此,那麼第一步的阻塞完全沒有必要啊!非阻塞呼叫,然後繼續處理其他任務豈不是更好。

因此,壓根不存在非同步阻塞這種模型哦

5. 千萬分清主語是誰

最後給各位提個醒,和別人討論阻塞非阻塞的時候千萬要帶上主語。

如果我問你,epoll是阻塞還是非阻塞?你怎麼回答?

應該說,epoll_wait這個函式本身是阻塞的,但是epoll會將socket設定為非阻塞。因此單純把epoll認為阻塞是太委屈它,認為其是非阻塞又抬舉它。

具體關於epoll的說明可以參見IO多路複用中的epoll部分。


完~

相關文章