《大前端進階 Node.js》系列 非同步非阻塞(阻塞究竟是指什麼?)

接水怪發表於2020-04-06

前言

Coding 應當是一生的事業,而不僅僅是 30 歲的青春飯
本文已收錄 GitHub https://github.com/ponkans/F2E,歡迎 Star,持續更新

面試官問:阻塞是用來形容什麼的?

如果你還要再三思考這個問題(面試官此時心裡絕壁在想,這 tm 還要思考,還跟我談什麼 Node 非同步非阻塞!),請好好看下面的文章。


每篇文章都希望你能收穫到東西,這篇是講 Node 非同步非阻塞的原理,看完希望你有這些收穫:

  • 阻塞、非阻塞本質與區別
  • 同步、非同步本質與區別
  • Node 非同步非阻塞本質

PS:底層基礎決定上層建築,請小夥伴們重視基礎哦~


非同步非阻塞

在提到 Node 的時候,非同步非阻塞是一個經常被提及的話題,與之伴隨的還有事件、回撥、訊息等等一系列詞語。

看這些概念就像追一個渣女,你好像覺得自己很懂她,但有時候你又會覺得一無所知

本文將帶大家層層剖析,自底向上的深入理解這些概念,讓你看清這個渣女的真面目。

阻塞非阻塞

很多人會把非阻塞和非同步混淆,這兩個概念本身也確實有相似之處,但本質上肯定是不一樣的,不然就不會被分成兩個名詞了。

我們先要思考的是阻塞是用來形容什麼的?答案自然是程式。程式的五大狀態:建立、就緒、執行、阻塞、終止。所以我們講的阻塞非阻塞,一定是指程式。

明確了這一點之後,我們再來看這個概念,當一個程式在發起一個呼叫的時候,如果這個程式從執行態變成阻塞態,那就說明這是一次阻塞呼叫,反之就是非阻塞。

同步與非同步

我們先搞清楚同步非同步形容的是啥?我們常說的是某某方法是個非同步方法,或者是某某呼叫是一種非同步呼叫。可見同步非同步形容的是某個呼叫的特性

何為同步?就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回。按照這個定義,其實絕大多數函式都是同步呼叫。

非同步的概念和同步相對。當一個非同步功能呼叫發出後,呼叫者不能立刻得到結果。當該非同步功能完成後,通過狀態、通知或回撥來通知呼叫者。

這裡有兩個重要的點:

  • 第一是可以在得到結果前就直接返回,無需呼叫阻塞執行緒等待。
  • 第二就是能夠在結束前主動的去通知主執行緒,並執行回撥。

對於上述解釋,可能有的小夥伴還是會有點迷茫,難以理清楚二者關係。彆著急,往下看。

響水壺

關於上面的概念,網上有一個很經典的響水壺解釋,怪怪在這裡引申給大家,並談談自己的理解。


隔壁王大爺(不是隔壁老王,hhhhh~~)有個水壺,王大爺經常用它來燒開水。

王大爺把水壺放到火上燒,然後啥也不幹在那等,直到水開了王大爺再去搞別的事情。(同步阻塞

王大爺覺得自己有點憨,不打算等了。把水壺放上去之後大爺就是去看電視,是不是來瞅一眼有沒有開(同步非阻塞

王大爺去買了個響水壺,他把響水壺放在火上,然後也是等著水開,水開的時候水壺會發出聲響(非同步阻塞

王大爺又覺得自己有點憨,他把響水壺放在火上然後去看電視,這時他不用是不是來瞅一眼,因為水開的時候水壺會發出聲音通知大爺。(非同步非阻塞

上面四個栗子裡,阻塞非阻塞說明的是大爺的狀態,同步非同步說明的是水壺的呼叫姿勢。水壺能在燒好的時候主動響起,就等同於我們非同步的定義,能在結束時通知主執行緒並且回撥。所以非同步一般配合非阻塞,才能發揮其作用

阻塞 IO 與 非阻塞 IO

有了上面王大爺的啟發,大家對一些基本的概念或許有了認知,那我們來進一步討論下非阻塞 IO 與非同步 IO。

阻塞 IO

阻塞 IO 如同其名字,主執行緒會在呼叫 IO 方法時進入阻塞態,直到 IO 結果返回,再繼續執行,相當於需要整個操作全部結束了,呼叫才會返回。

首先要知道讀一個磁碟的開銷,讀磁碟涉及到磁碟尋道,在對應扇區讀取資料,然後把資料放在記憶體等一系列操作。所以阻塞 IO 必然是會被取代的,具體可以看下面這張圖。

阻塞IO
阻塞IO
非阻塞 IO

非阻塞 IO 的特點與阻塞相對,在作業系統發起 IO 呼叫之後,可以先不帶資料直接返回,這樣主執行緒不會被阻塞,然後作業系統來處理讀磁碟這一系列操作,而不需要主程式被阻塞,這就是我們所說的非阻塞 IO 了。


這裡可以順便提一下,現在大多數 IO 裝置都支援DMA(Direct Memory Access,直接儲存器訪問),DMA 的意義在於可以解放 IO 時處理器的壓力,CPU 只需要 DMA 控制器初始化,並向 I/O 介面發出操作命令,I/O 介面提出 DMA 請求,然後在儲存器和外部裝置之間直接進行資料傳送,在傳送過程中不需要中央處理器的參與,這段時間 CPU 可以去執行別的任務。


回到我們的非阻塞 IO,他的好處顯而易見,程式不用等待函式返回,可以做做別的事情。

但也有一個明顯的缺陷,我們想要讀取的時候在函式返回時並沒有就位。個人的理解是,如果你接下來的操作馬上就強依賴 IO 的資料,那阻塞與否並無區別。

如果你接下來的操作並非強依賴,那可以先把非強依賴的程式執行了,再去看 IO 有沒有好,這樣 cpu 等待 IO 這段時間就可以被利用起來。非阻塞 IO 大致過程如下圖。

PS:這裡的阻塞和非阻塞和之前的觀點一致,看的是發起呼叫的程式有沒有阻塞。

非阻塞IO
非阻塞IO

輪詢技術演進

上面講到了一個關鍵的點,非阻塞 IO 的時候,我們想要讀取的時候在函式返回時並沒有就位。

就像那個燒水的大爺,在他沒有響水壺的時候,他雖然一邊燒水一邊看電視,但他是不是也要去看一下水到底有沒有開。

我這裡也一樣,我們無法預知資料什麼時候好,所以我們也要去主動的探查 IO 資料是否就位,因為是我們主動的探查,那可以確定的就是,輪詢技術並非非同步,他並不是一個響水壺


這裡需要幫大家梳理清楚一個細節,可能大家經常能聽到很多 IO 相關的名詞,比如 recv,select, epoll, kqueue 等等,但對他們可能沒有很直觀的認知。我們要讀取一個檔案,是分為兩步的。

首先是去讀取檔案,然後是獲取讀取的結果。

讀取檔案需要呼叫的是 recv,recv 可以根據引數來決定是否阻塞,我們所討論的非阻塞 IO,只是在讀取這一步,而 select、epoll 都是第二步(獲取結果)做的事情,他們是阻塞的方法。大家切莫把兩個步奏混為一談。

結下來我們來捋一下我們的輪詢技術。

初代:read

read 是最原始的輪詢方式,read 本身就是讀取檔案的方法,在 C++的呼叫裡面,如果設定了 NONBLCOK 屬性,那就會立即返回,但返回的值是-1。

簡單寫了個 C 的 read 供大家參考,加深下理解。先是阻塞 read。

阻塞IO
阻塞IO

那麼之後我們需要這個 IO 結果的時候咋辦呢?只能不斷的呼叫 read 方法,直到他的返回不是-1 為止,然後從傳入的一個 char[] 中拿檔案資料,程式碼大致如下圖。

非阻塞IO
非阻塞IO

流程大致如下圖,雖然在發起 IO 的時候非阻塞了,但其弊端顯而易見,他需要在獲取的時候不斷的去輪詢,這裡會很耗費 cpu,如果這是一個多核機器可能還好,如果是單核,那一個 cpu 被這些沒有意義的輪詢耗在這裡,就很憨(怕不是個鐵憨憨吧)。

非阻塞IO
非阻塞IO
進階:select

之前我們講了 read,read 的弊端是很明顯的,需要不斷輪詢,除此之外我們只能監聽一個檔案,比如我需要讀兩個檔案,那意味著我會呼叫兩次 read 方法,這個時候我想獲取結果的話就需要先輪詢一個,等那個返回了,再輪詢另一個。

你可能會說,我在一個 while(True)裡面寫兩個不斷呼叫 read 的程式碼不就行了。

那我們如果要讀 10 個檔案?100 個檔案?你要寫 100 個嗎?答案肯定是 NO。針對不斷的空輪詢問題,和多檔案監聽問題,作業系統給出了更優的解決方案,select 呼叫。

我們在發起 read 操作之後,能拿到一個 fd(檔案描述符),對檔案描述符不理解的小夥伴可以去翻一下怪怪之前寫的《大前端進階 Node.js》系列 多程式模型底層實現,裡面有詳細描述。

如果我們發起 100 次 read 呼叫,那就會有 100 個 fd,select 可以批量監聽檔案描述符,我們在呼叫 select 方法的時候,當前程式進入阻塞狀態(注意,之前講的非阻塞 IO 是 read 呼叫,select 是阻塞呼叫)。

當監聽的這一批檔案描述符裡,有屬於某個檔案描述符的 IO 操作結束的時候,作業系統會發起中斷,中斷程式做的事情很簡單,喚醒阻塞的程式。

這個時候意味著某個檔案描述符的資料已經就緒了,但問題是哪一個呢?母雞。咋辦呢?輪詢。把所有監聽的 fd 掃一遍,取出就緒的檔案描述符,讀取響應資料。

這樣的話,之前兩個問題就得到了解決,輪詢消耗 cpu 問題通過阻塞程式,中斷喚醒來解決,多檔案監聽問題通過 select 的多控制程式碼監聽特性來搞定。

大致流程如下圖。

演進:epoll

select 解決了大多數的問題,但卻帶來了新的問題,如上面描述的一樣,程式在被喚醒的時候一臉迷茫,是誰喚醒的我??他要一個一個看。

如果檔案過多,這種遍歷對效能的影響是很大的,所以 select 設計之初便規定監聽的檔案描述符是有上限的,一般是 1024 個。

為了解決 select 留下的坑,誕生了我們現在用的最廣泛的 epoll。

epoll 最關鍵的優化點在於,引入了一個介於程式和 fd 之間的東西:eventpoll

在有 IO 結束的時候,中斷程式不是直接喚醒程式,而是會先把 IO 就緒的檔案描述符放在 eventpoll 裡面,後續在程式被喚醒後,不需要輪詢整個 fd 列表,只需要在 eventpoll 裡面拿就緒的檔案描述符即可。

以上就是目前主流輪詢技術的演進過程了~

下期預告,Node 之非同步非阻塞 IO(下)

之前講過,read 提供了非阻塞的 IO 呼叫方式,但系統在需要資料的時候,需要主動去獲取,而不是非同步的被通知,epoll 再美好,終究不是一個響水壺

那是不是沒有響水壺的存在呢?倒也不是,Linux 提供了 AIO 模式,是基於訊號和回撥的原生非同步介面,但不幸的是這玩意只有 Linux 有,而且存在一定的缺陷(這裡不展開講了,有興趣可以留言,往後安排一期來講)。

本文一開始就說了,非同步非阻塞是 Node 的特性,它騙人?不,我們上面討論的各種觀點都是基於單程式來討論的,如果利用其多程式,雖然作業系統層面不支援響水壺,Node 可以自己來做一個。

eventloop + 執行緒池

如我上面的小標題,實現非同步非阻塞 IO 的原理就是 eventloop+執行緒池。Node 在不同的平臺會對應不同的系統呼叫,但無論如何,基本的架構大同小異

具體的 Node 非同步非阻塞 IO 架構,敬請期待怪怪的《大前端進階 Node.js》系列 非同步非阻塞(下)。

總結

本文已收錄 GitHub https://github.com/ponkans/F2E,歡迎 Star,持續更新

這篇是基礎,必要要先掌握,後面的理解起來才會有如魚得水的感覺~

這篇文章裡,怪怪主要幫大家捋清了很多容易混淆的概念,並從作業系統的角度介紹了我們目前 IO 的狀況。總結下就是:

  • 阻塞非阻塞看程式狀態。
  • 非同步非非同步看呼叫的特性。
  • read 是同步非阻塞,可以看作是非阻塞 IO,但獲取結果資料的方法是一種同步阻塞的方法,常用的就是上面講的 epoll。

PS:看過網上部分文章,都沒講在點上,理解這個東西需要從最初,最本質去理解它,你才會真正的懂這個架構!

那 Node 如何非同步非阻塞?eventLoop 到底是個啥?留到本系列(下)來講。

近期原創傳送門,biubiubiu:


喜歡的小夥伴加個關注,點個贊哦,感恩??

聯絡我 / 公眾號

微信搜尋【接水怪】或掃描下面二維碼回覆”加群“,我會拉你進技術交流群。講真的,在這個群,哪怕您不說話,光看聊天記錄也是一種成長。(阿里技術專家、敖丙作者、Java3y、蘑菇街資深前端、螞蟻金服安全專家、各路大牛都在)。

接水怪也會定期原創,定期跟小夥伴進行經驗交流或幫忙看簡歷。加關注,不迷路,有機會一起跑個步? ↓↓↓

相關文章