首頁 > web前端 > js教程 > 主體

聊聊Node.js中的進程、執行緒、協程與並發模型

青灯夜游
發布: 2022-07-19 20:11:07
轉載
2312 人瀏覽過

聊聊Node.js中的進程、執行緒、協程與並發模型

Node.js 現在已成為建置高並發網頁應用程式服務工具箱中的一員,何以 Node.js 會成為大眾的寵兒?本文將從進程、執行緒、協程、I/O 模型這些基本概念說起,為大家全面介紹關於 Node.js 與並發模型的這些事。

進程

我們一般將某個程式正在執行的實例稱為進程,它是作業系統進行資源分配和調度的一個基本單元,一般包含以下幾個部分:

  • 程式:即要執行的程式碼,用於描述進程要完成的功能;
  • 資料區域:處理程序的資料空間,包括資料、動態分配的記憶體、處理函數的使用者堆疊、可修改的程式等資訊;
  • 行程表項:為了實作進程模型,作業系統維護著一張稱為行程表的表格,每個進程佔用一個進程表項(也叫進程控制塊),該表項包含了程式計數器、堆疊指標、記憶體分配情況、所開啟文件的狀態、調度資訊等重要的進程狀態訊息,從而確保進程掛起後,作業系統能夠正確地重新喚起該進程。

進程具有以下特徵:

  • 動態:進程的實質是程式在多道程式系統中的一次執行過程,進程是動態產生,動態消亡的;
  • 並發性:任何進程都可以與其他進程一起並發執行;
  • 獨立性:進程是一個能獨立運作的基本單位,同時也是系統分配資源和調度的獨立單位;
  • 非同步性:由於進程間的相互制約,使進程具有執行的間斷性,即進程按各自獨立的、不可預測的速度向前推進。

要注意的是,如果一個程式運行了兩遍,即使作業系統能夠使它們共享程式碼(即只有一份程式碼副本在記憶體中),也不能改變正在執行的程式的兩個實例是兩個不同的進程的事實。

在進程的執行過程中,由於中斷、CPU 調度等各種原因,進程會在下面幾個狀態中切換:

聊聊Node.js中的進程、執行緒、協程與並發模型

  • 運行態:此刻進程正在運行,並且佔用了CPU;
  • 就緒態:此刻進程已準備就緒,隨時可以運行,但因為其它進程正在運行而被暫時停止;
  • 阻塞態:此刻進程處於阻塞狀態,除非某個外部事件(例如鍵盤輸入的資料已到達)發生,否則進程將無法運作。

透過上面的進程狀態切換圖可知,行程可以從運行態切換成就緒態和阻塞態,但只有就緒態才​​能直接切換成運行態,這是因為:

  • 從運行態切換成就緒態是由進程調度程序引起的,因為系統認為當前進程已經佔用了過多的CPU 時間,決定讓其它進程使用CPU 時間;並且進程調度程序是操作系統的一部分,進程甚至感覺不到調度程序的存在;
  • 從運行態切換成阻塞態是由進程自身原因(例如等待用戶的鍵盤輸入)導致進程無法繼續執行,只能掛起等待某個事件(例如鍵盤輸入的資料已到達)發生;當相關事件發生時,進程先轉換為就緒態,如果此時沒有其它進程運行,則立刻轉換為運行態,否則進程將維持就緒態,等待進程調度程序的調度。

線程

有些時候,我們需要使用執行緒來解決以下問題:

  • 隨著進程數量的增加,進程之間切換的成本將越來越大,CPU 的有效使用率也會越來越低,嚴重情況下可能造成系統假死等現象;
  • 每個進程都有自己獨立的記憶體空間,且各個進程之間的記憶體空間是相互隔離的,而某些任務之間可能需要共享一些數據,多個進程之間的數據同步就過於繁瑣。

關於線程,我們需要知道以下幾點:

  • 執行緒是程式執行中的單一順序控制流,是作業系統能夠進行運算調度的最小單位,它包含在進程之中,是進程中的實際運行單位;
  • 一個進程中可以包含多個線程,每個線程並行執行不同的任務;
  • 一個進程中的所有執行緒共享進程的記憶體空間(包括程式碼、資料、堆等)以及一些資源資訊(例如開啟的檔案和系統訊號);
  • 一個行程中的執行緒在其它進程中不可見。

了解了執行緒的基本特徵,下面我們來聊聊常見的幾種執行緒類型。

核心狀態線程

核心態線程是直接由作業系統支援的線程,其主要特點如下:

  • 執行緒的建立、調度、同步、銷毀由系統核心完成,但其開銷較為昂貴;
  • 核心可將核心態執行緒映射到各個處理器上,能夠輕鬆做到一個處理器核心對應一個核心線程,從而充分地競爭與利用CPU 資源;
  • 僅能存取核心的程式碼和資料;
  • 資源同步與資料共享效率低於進程的資源同步與數據共享效率。

使用者狀態線程

使用者狀態線程是完全建立在使用者空間的線程,其主要特點如下:

  • #執行緒的建立、調度、同步、銷毀由使用者空間完成,其開銷非常低;
  • 由於使用者狀態執行緒由使用者空間維護,核心根本感知不到使用者態執行緒的存在,因此核心僅對其所屬的行程做調度及資源分配,而行程中執行緒的調度及資源分配由程式自行處理,這很可能造成一個使用者態執行緒被阻塞在系統呼叫中,則整個行程都會阻塞的風險;
  • 能夠存取所屬程序的所有共享位址空間和系統資源;
  • 資源同步與資料共享效率較高。

輕量級進程(LWP)

輕量級進程(LWP)是建立在內核之上並由核心支援的用戶線程,其主要特點如下:

  • 用戶空間只能透過輕量級進程(LWP)來使用核心線程,可看作是用戶狀態線程與核心線程的橋接器,因此只有先支援內核線程,才能有輕量級進程(LWP);

  • 大多數輕量級進程(LWP)的操作,都需要用戶態空間發起系統調用,此系統調用的代價相對較高(需要在用戶態與內核態之間進行切換);

  • 每個輕量級進程(LWP)都需要與一個特定的內核執行緒關聯,因此:

    • 與核心執行緒一樣,可在全系統範圍內充分地競爭與利用CPU 資源;
    • 每個輕量級進程(LWP)都是一個獨立的執行緒調度單元,這樣即使有一個輕量級進程(LWP)在系統呼叫中被阻塞,也不影響整個進程的執行;
    • 輕量級進程(LWP)需要消耗核心資源(主要指核心執行緒的堆疊空間),這樣導致系統中不可能支援大量的輕量級進程(LWP);
  • 能夠存取所屬進程的所有共享位址空間和系統資源。

小結

上文我們對常見的執行緒類型(核心態執行緒、使用者狀態執行緒、輕量級進程)進行了簡單介紹,它們各自有各自的適用範圍,在實際的使用中可根據自己的需要自由地對其進行組合使用,比如常見的一對一、多對一、多對多等模型,由於篇幅限制,本文對此不做過多介紹,有興趣的同學可自行研究。

協程

協程(Coroutine),也叫纖程(Fiber),是一種建立在執行緒之上,由開發者自行管理執行調度、狀態維護等行為的一種程式運作機制,其特點主要有:

  • 因執行調度無需上下文切換,故具有良好的執行效率;
  • 因運行在同一線程,故不存在線程通訊中的同步問題;
  • 方便切換控制流,簡化程式設計模型。

在JavaScript 中,我們常用到的async/await 便是協程的一種實現,例如下面的例子:

function updateUserName(id, name) {
  const user = getUserById(id);
  user.updateName(name);
  return true;
}

async function updateUserNameAsync(id, name) {
  const user = await getUserById(id);
  await user.updateName(name);
  return true;
}
登入後複製

上例中,函數updateUserNameupdateUserNameAsync 內的邏輯執行順序是:

    ##呼叫函數
  • getUserById 並將其傳回值賦給變數user;
  • 呼叫
  • userupdateName 方法;
  • 傳回
  • true 給呼叫者。
兩者的主要差異在於其實際運行過程中的狀態控制:

    #在函數
  • updateUserName 的執行過程中,依照前文所述的邏輯順序依序執行;
  • 在函數
  • updateUserNameAsync 的執行過程中,同樣依照前文所述的邏輯順序依序執行,只不過在遇到await時,updateUserNameAsync 將會被掛起並儲存掛起位置目前的程式狀態,直到await 後面的程式片段回傳後,才會再次喚醒updateUserNameAsync並恢復掛起前的程式狀態,然後繼續下一段程式。
透過上面的分析我們可以大膽猜測:協程要解決的並非是進程、執行緒要解決的程式並發問題,而是要解決處理非同步任務時所遇到的問題(例如檔案操作、網路請求等);在

async/await 之前,我們只能透過回呼函數來處理非同步任務,這很容易使我們陷入回呼地獄,生產出一坨坨屎一般難以維護的程式碼,透過協程,我們便可以實現非同步程式碼同步化的目的。

要牢記的是:協程的核心能力是能夠將某段程式掛起並維護程式掛起位置的狀態,並在未來某個時刻在掛起的位置恢復,並繼續執行掛起位置後的下一段程序。

I/O 模型

一個完整的I/O 操作需要經歷以下階段:

  • 用戶進(線)程透過系統呼叫向核心發起I/O 操作請求;
  • 核心對I/O 操作請求進行處理(分為準備階段和實際執行階段),並將處理結果傳回使用者進(線)程。

我們可將I/O 操作大致分為阻塞I/O非阻塞I/O同步I/O非同步I/O 四種類型,在討論這些類型之前,我們先熟悉下以下兩組概念(此處假設服務A 呼叫了服務B):

  • 阻塞/非阻塞

    • 如果A 只有在接收到B 的回應之後才傳回,那麼該呼叫為阻塞呼叫;
    • 如果A 呼叫B 後立即返回(即無需等待B 執行完畢),那麼該呼叫為非阻塞呼叫
  • 同步/非同步

    • #如果B 只有在執行完之後再通知A,那麼服務B 是同步的;
    • 如果A 呼叫B 後,B 立刻給A 一個請求已接收的通知,然後在執行完之後透過回呼的方式將執行結果通知給A,那麼服務B 就是非同步的。

很多人經常將阻塞/非阻塞同步/非同步搞混淆,故需要特別注意:

  • 阻塞/非阻塞針對於服務的呼叫者而言;
  • ##同步/非同步針對於服務的被呼叫者而言。
了解了

阻塞/非阻塞同步/非同步,我們來看特定的 I/O 模型

阻塞I/O

定義:用戶進(線)程發起

I/O 系統呼叫後,用戶進(線)程會立即被阻塞,直到整個I/O 作業處理完畢並將結果回傳給使用者進(線)程後,使用者進(線)程才能解除阻塞狀態,繼續執行後續操作。

特點:

    由於此模型會阻塞使用者進(線)程,因此此模型不佔用CPU 資源;
  • 在執行
  • I/ O 操作的時候,用戶進(線)程不能進行其它操作;
  • 該模型僅適用於並發量小的應用,這是因為一個
  • I/O 請求就能阻塞進(線)程,所以為了能夠及時回應I/O 請求,需要為每個請求分配一個進(線)程,這樣會造成巨大的資源佔用,並且對於長連接請求來說,由於進(線)程資源長期無法釋放,如果後續有新的請求,將會產生嚴重的效能瓶頸。

非阻塞I/O

定義:

    用戶進(線)程發起
  • I/O 系統呼叫後,如果該I/O 操作未準備就緒,則該I/O 呼叫將會傳回錯誤,使用者進(線)程也無需等待,而是透過輪詢的方式來偵測該I/O 操作是否就緒;
  • 操作就緒後,實際的
  • I/O 操作會阻塞使用者進(線)程直到執行結果返回給用戶進(線)程。
特點:

    由於該模型需要使用者進(線)程不斷地詢問
  • I/O 操作就緒狀態(一般使用while 循環),因此此模型需佔用CPU,消耗CPU 資源;
  • I/O 操作就緒前,使用者進(線)程不會阻塞,等到I/O 操作就緒後,後續實際的I/O 操作將阻塞使用者進(線)程;
  • 此模型僅適用於並發量小,且不需要及時響應的應用。

同(異)步I/O

用戶進(線)程發起

I/O 系統呼叫後,如果此I/O 呼叫會導致使用者進(線)程阻塞,那麼該I/O 呼叫便為同步I/O,否則為非同步I/O

判斷

I/O 操作同步非同步的標準是用戶進(線)程與I/O 操作的通訊機制,其中:

  • 同步情況下用戶進(線)程與I/O 的交互是透過內核緩衝區進行同步的,即內核會將I /O 操作的執行結果同步到緩衝區,然後再將緩衝區的資料複製到用戶進(線)程,這個過程會阻塞用戶進(線)程,直到I/O 操作完成;
  • 非同步情況下用戶進(線)程與I/O 的互動是直接透過核心進行同步的,即核心會直接將I/O 操作的執行結果複製到使用者進(線)程,這個過程不會阻塞使用者進(線)程。

Node.js 的並發模型

Node.js 採用的是單執行緒、基於事件驅動的非同步I/O模型,個人認為之所以選擇該模型的原因在於:

  • JavaScript 在V8 下以單執行緒模式運行,為其實現多執行緒極其困難;
  • #絕大多數網絡應用程式都是I/O 密集的,在保證高並發的情況下,如何合理、有效率地管理多執行緒資源相對於單執行緒資源的管理更加複雜。

總之,本著簡單、高效的目的,Node.js 採用了單執行緒、基於事件驅動的非同步I/O 模型,並透過主執行緒的EventLoop 和輔助的Worker 執行緒來實現其模型:

  • Node.js 進程啟動後,Node.js 主執行緒會建立一個EventLoop,EventLoop 的主要作用是註冊事件的回呼函數並在未來的某個事件循環中執行;
  • Worker 執行緒用來執行特定的事件任務(在主執行緒之外的其它執行緒中以同步方式執行),然後將執行結果傳回主執行緒的EventLoop 中,以便EventLoop 執行相關事件的回呼函數。

要注意的是,Node.js 並不適合執行CPU 密集型(即需要大量計算)任務;這是因為EventLoop 與JavaScript 程式碼(非非同步事件任務程式碼)運行在同一線程(即主執行緒),它們中任何一個如果運行時間過長,都可能導致主執行緒阻塞,如果應用程式中包含大量需要長時間執行的任務,將會降低伺服器的吞吐量,甚至可能導致伺服器無法回應。

總結

Node.js 是前端開發人員現在甚至未來不得不面對的技術,然而大多數前端開發人員對Node.js 的認知僅停留在表面,為了讓大家更能理解Node.js 的並發模型,本文先介紹了進程、線程、協程,接著介紹了不同的I/O 模型,最後對Node.js的並發模型進行了簡單介紹。雖然介紹 Node.js 並發模型的篇幅不多,但筆者相信萬變不離其宗,掌握了相關基礎,再深入理解 Node.js 的設計與實現必將事半功倍。

最後,本文若有紕漏之處,也望大家能夠指正,祝大家快樂編碼每一天。

更多node相關知識,請造訪:nodejs 教學

以上是聊聊Node.js中的進程、執行緒、協程與並發模型的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!