首頁  >  文章  >  web前端  >  初步了解Nodejs中的非同步I/O

初步了解Nodejs中的非同步I/O

青灯夜游
青灯夜游轉載
2021-04-19 10:08:061987瀏覽

本篇文章帶大家初步了解下Nodejs中的非同步I/O。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

初步了解Nodejs中的非同步I/O

「非同步」這個名詞其實在Node之前就已經誕生了。但是在絕大多數高階程式語言中,非同步並不多見。在眾多高階語言或運行平台中,將非同步作為主要程式設計方式和設計理念的,Node是首個。 【相關推薦:《nodejs 教學》】

異步I/O、事件驅動與單執行緒構成了Node的基調,而Nginx與Node的事件驅動、非同步I/O設計理念比較相近。 Nginx採用純C編寫,效能表現非常優異,具備面向客戶端管理連線的強大能力,但是它的背後依然受限於各種同步方式的程式語言。但Node是全方位的,既可以作為伺服器端去處理客戶端帶來的大量並發請求,也能作為客戶端向網路中的各個應用進行並發請求。

為什麼要異步I/O


為什麼非同步I/O在Node中如此重要,這是因為Node面向網路設計,在跨網路的結構下,並發已經是現代程式設計中的標準配備了。

使用者體驗

《高效能JavaScript》中提到過,如果腳本的執行時間超過100毫秒,使用者就會感到頁面卡頓,以為頁面停止回應。而在B/S模型中,網路速度的限制對網頁的即時體驗造成很大的麻煩。

如果網頁暫時需要取得一個資源,透過同步的方式獲取,那麼JavaScript則需要等待資源完全從伺服器端取得後才能繼續執行,這期間UI停頓,不回應使用者的互動行為。這樣使用者體驗將會極差。而採用非同步請求,在下載資源期間,JavaScript和UI的執行都不會處於等待狀態,可以繼續回應使用者的互動行為。

同理,前端透過非同步可以消除掉UI阻塞現象,但前端取得資源的速度也取決於後端的反應速度。假如一個資源來自於兩個不同位置的資料的返回,第一個資源消耗M毫秒,第二個資源消耗N毫秒。如果採用同步的方式,取得兩個資源消耗的時間為M N毫秒。而採用非同步的方式,第一個資源的取得並不會阻塞第二個資源的獲取,消耗的時間為max(M,N)。

隨著網站或應用程式不斷膨脹,M與N的值會線性成長,那麼非同步的效能將比同步更優越。

資源分配

假設業務場景中有一組互不相關的任務需要完成,有以下兩種主流的方法:

  • #單執行緒串列一次執行
  • 多執行緒並行完成

如果創建多執行緒的開銷小於並行執行,那麼多執行緒是首選的,但是多執行緒在建立執行緒和執行期執行緒上下文切換的開銷較大,多執行緒程式設計經常面臨鎖、狀態同步等問題。

單執行緒順序執行任務的缺點在於效能,任一略慢的任務都會導致後續執行程式碼被阻塞。在電腦資源中,通常I/O與CPU運算之間是可以並行執行的,但是同步的程式設計模型導致I/O的進行會讓後續任務等待,造成資源不能被更好的利用。

Node利用單線程,遠離多線程死鎖、狀態同步等問題;利用非同步I/O,讓單線程遠離阻塞,更好的利用CPU。

非同步I/O實作


非同步I/O在Node中應用最為廣泛,但它並不是Node的原創。

非同步I/O與非阻塞I/O

#對電腦核心I/O而言,非同步/同步與阻塞/非阻塞是兩碼事。

作業系統對於I/O只有兩種方式:阻塞和非阻塞。在呼叫阻塞I/O時,應用程式需要等待I/O完成才傳回結果。

阻塞I/O的一個特點是呼叫之後一定要等到系統核心層面完成所有操作之後,呼叫才結束。阻塞I/O造成CPU等待I/O,浪費等待時間,CPU的處理能力無法充分利用。

為了提高效能,核心提供了非阻塞I/O。非阻塞I/O跟阻塞I/O的差異為呼叫之後會立即返回,非阻塞I/O返回之後,CPU的時間片可以用來處理其他事物,此時提升性能是明顯的,但是由於完成的I/O並沒有完成,立即回傳的並不是業務層期望的數據,而只是當前的呼叫狀態。

為了取得完整的數據,應用程式需要重複呼叫I/O操作來確認是否完成。這種重複呼叫判斷操作是否完成的技術叫做輪詢

現存的輪詢技術主要有read、select、poll、epoll和kqueue。這裡只講一下epoll的輪詢原理。

epoll是Linux下效率最高的I/O事件通知機制,在進入輪詢的時候,如果沒有檢查到I/O事件,將會進行休眠,直到事件發生將它喚醒。它是真實利用了事件的通知、執行回呼的方式,而不是遍歷查詢,所以不會浪費CPU,執行效率較高。

初步了解Nodejs中的非同步I/O

輪詢技術滿足了非阻塞I/O確保獲取完整資料的需求,但是對於程式而言,它仍然算是一種同步,因為應用程式仍然需要等待I/O完全返回,依舊花了很多時間等待。等待期間,CPU要麼用於遍歷檔案描述符,要麼用於休眠等待時間發生。

現實的非同步I/O

透過讓部分執行緒進行阻塞I/O或非阻塞I/O加輪詢技術來完成資料獲取,讓一個線程進行計算處理,透過線程之間的通信將I/O得到的數據進行傳遞,這就輕鬆實現了異步I/O(雖然這是模擬的)

但是最初,Node在*nix平台下採用了libeio配合libev實作I/O部分,實作了非同步I/O。在Node v0.9.3中,自行實作了執行緒池來完成非同步I/O。

而Windows下的IOCP在某種程度上提供了黎翔的非同步I/O:呼叫非同步方法,等待I/O完成之後的通知,執行回調,使用者無需考慮輪詢。但是它的內部其實還是線程池原理,不同之處在於這些執行緒池有系統核心接手管理。

由於Windows平台和*nix平台的差異,Node提供了libuv作為抽象封裝層,使得所有平台相容性的判斷都由這一層來完成,並保證上層的Node與下層的自定義線程池及IOCP之間個字獨立。

初步了解Nodejs中的非同步I/O

我們時常提到Node是單執行緒的,這裡的單一執行緒只是JavaScript執行在單一執行緒中。在Node中,無論是*nix或Windows平台,內部完成I/O任務的另一個執行緒池。

Node的非同步I/O


完成整個非同步I/O環節的有事件循環、觀察者和請求物件等。

事件循環

事件循環是Node本身的執行模型,而正式它使得回呼函數十分普遍。

在進程啟動時,Node就會建立一個類似while(true)的循環,每執行一次循環體的過程我們稱為Tick。每個Tick的過程就是查看是否有事件待處理,如果有就取出事件及其相關的回呼函數。如果存在關聯的回呼函數,就執行他們。然後進入下個循環,如果不再有事件處理,就退出進程。

初步了解Nodejs中的非同步I/O

觀察者

每個事件循環中有一個或多個觀察者,而判斷是否有事件要處理的過程就是向這些觀察者詢問是否有要處理的事件。

在Node中,事件主要來自網路請求、檔案I/O等,這些時間對應的觀察者有檔案I/O觀察者、網路I/O觀察者等。觀察者將事件進行了分類。

事件循環是一個典型的生產者/消費者模式。非同步I/O、網路請求等則是事件的生產者,不斷為Node提供不同類型的事件,這些事件被傳遞到對應的觀察者那裡,事件循環則從觀察者那裡取出事件並處理。

請求物件

對於Node的非同步I/O呼叫而言,回呼函數不由開發者來呼叫。從JavaScript發起呼叫到核心執行完I/O操作的過渡過程中,存在一種產物,叫做請求物件

#下面用fs.open()方法作為一個小小的例子。

fs.open = function(path,flags,mode,callback){
    //...
    binding.open(pathModule._makeLong(path),
                    stringToFlags(flags),
                    mode,
                    callback);
}

fs.open()的作用是根據指定路徑和參數去開啟一個文件,從而得到一個文件描述符,這是後續所有I/O操作的初試操作。 JavaScript層面的程式碼透過呼叫C 核心模組進行下層的操作。

初步了解Nodejs中的非同步I/O

從事JavaScript調用Node的核心模組,核心模組調用C 模組,內建模區塊透過libuv進行系統調用,這裡是Node裡經典的調用方式。這裡libuv作為封裝層,有兩個平台的實現,實質上是呼叫了uv_fs_open()方法。在uv_fs_open()的呼叫過程中,將從JavaScript層傳入的參數和目前方法都封裝在一個請求物件中,回呼函數則被設定在這個物件的屬性上。物件包裝完畢後,將物件推入線程池等待執行。

至此,JavaScript呼叫立即返回,由JavaScript層面發起的非同步呼叫的第一階段就此結束。 JavaScript執行緒可以繼續執行目前任務的後續操作。

請求物件是非同步I/O過程中的重要中間產物,所有的狀態都保存在這個物件中,包括送入執行緒池等待執行以及I/O操作完畢後的回調處理。

執行回呼

組裝好請求物件、送入I/O執行緒池等待執行,只是完成一部I/O的第一部分,回呼通知是第二部分。

執行緒池中的I/O作業調用完畢之後,會將取得的結果儲存在req->result屬性上,然後呼叫PostQueueCompletionStatus()通知IOCP,告知目前物件操作已經完成。

至此,整個非同步I/O的流程完全結束。

初步了解Nodejs中的非同步I/O

事件循環、觀察者、請求物件、I/O執行緒池這四者共同構成​​了Node非同步I/O模型的基本要素。

小結

整理下來,我們可以提取非同步I/O的幾個關鍵字:單執行緒、事件循環、觀察者和I/O執行緒池。單線程和線程池看起來有些悖論的樣子。因為JavaScript是單執行緒的,所以很容易理解為它不能充分利用多核心CPU。事實上,在Node中,除了JavaScript是單執行緒外,Node本身其實是多執行緒的,只是I/O執行緒使用的CPU較少。還有就是除了使用者程式碼無法並行執行外,所有的I/O(磁碟I/O和網路I/O等)都是可以並行起來的。

更多程式相關知識,請造訪:程式設計影片! !

以上是初步了解Nodejs中的非同步I/O的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除