現在已成為構建高並行網路應用服務工具箱中的一員,何以 Node.js 會成為大眾的寵兒?本文將從程序、執行緒、協程、I/O 模型這些基本概念說起,為大家全面介紹關於 Node.js 與並行模型的這些事。
我們一般將某個程式正在執行的範例稱之為程序,它是作業系統進行資源分配和排程的一個基本單元,一般包含以下幾個部分:
程序表
的表格,每個程序佔用一個程序表項
(也叫過程控制塊
),該表項包含了程式計數器、堆疊指標、記憶體分配情況、所開啟檔案的狀態、排程資訊等重要的程序狀態資訊,從而保證程序掛起後,作業系統能夠正確地重新喚起該程序。程序具有以下特徵:
需要注意的是,如果一個程式執行了兩遍,即便作業系統能夠使它們共用程式碼(即只有一份程式碼副本在記憶體中),也不能改變正在執行的程式的兩個範例是兩個不同的程序的事實。
在程序的執行過程中,由於中斷、CPU 排程等各種原因,程序會在下面幾個狀態中切換:
通過上面的程序狀態切換圖可知,程序可以從執行態切換成就緒態和阻塞態,但只有就緒態才能直接切換成執行態,這是因為:
有些時候,我們需要使用執行緒來解決以下問題:
關於執行緒,我們需要知道以下幾點:
瞭解了執行緒的基本特徵,下面我們來聊一下常見的幾種執行緒型別。
核心態執行緒是直接由作業系統支援的執行緒,其主要特點如下:
使用者態執行緒是完全建立在使用者空間的執行緒,其主要特點如下:
輕量級程序(LWP)是建立在核心之上並由核心支援的使用者執行緒,其主要特點如下:
使用者空間只能通過輕量級程序(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; }
上例中,函數 updateUserName
和 updateUserNameAsync
內的邏輯執行順序是:
getUserById
並將其返回值賦給變數 user
;user
的 updateName
方法;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
四種型別,在討論這些型別之前,我們先熟悉下以下兩組概念(此處假設服務 A 呼叫了服務 B):
阻塞/非阻塞
:
阻塞呼叫
;非阻塞呼叫
。同步/非同步
:
同步
的;回撥
的方式將執行結果通知給 A,那麼服務 B 就是非同步
的。很多人經常將阻塞/非阻塞
與同步/非同步
搞混淆,故需要特別注意:
阻塞/非阻塞
針對於服務的呼叫者
而言;同步/非同步
針對於服務的被呼叫者
而言。瞭解了阻塞/非阻塞
與同步/非同步
,我們來看具體的 I/O 模型
。
定義:使用者進(線)程發起 I/O
系統呼叫後,使用者進(線)程會被立即阻塞
,直到整個 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
操作的執行結果複製到使用者進(線)程,這個過程不會阻塞使用者進(線)程。Node.js 採用的是單執行緒、基於事件驅動的非同步 I/O
模型,個人認為之所以選擇該模型的原因在於:
I/O
密集型的,在保證高並行的情況下,如何合理、高效地管理多執行緒資源相對於單執行緒資源的管理更加複雜。總之,本著簡單、高效的目的,Node.js 採用了單執行緒、基於事件驅動的非同步 I/O
模型,並通過主執行緒的 EventLoop 和輔助的 Worker 執行緒來實現其模型:
需要注意的是,Node.js 並不適合執行 CPU 密集型(即需要大量計算)任務;這是因為 EventLoop 與 JavaScript 程式碼(非非同步事件任務程式碼)執行在同一執行緒(即主執行緒),它們中任何一個如果執行時間過長,都可能導致主執行緒阻塞,如果應用程式中包含大量需要長時間執行的任務,將會降低伺服器的吞吐量,甚至可能導致伺服器無法響應。
Node.js 是前端開發人員現在乃至未來不得不面對的技術,然而大多數前端開發人員對 Node.js 的認知僅停留在表面,為了讓大家更好地理解 Node.js 的並行模型,本文先介紹了程序、執行緒、協程,接著介紹了不同的 I/O
模型,最後對 Node.js 的並行模型進行了簡單介紹。雖然介紹
Node.js 並行模型的篇幅不多,但筆者相信萬變不離其宗,掌握了相關基礎,再深入理解 Node.js 的設計與實現必將事半功倍。
最後,本文若有紕漏之處,還望大家能夠指正,祝大家快樂編碼每一天。
更多node相關知識,請存取:!
以上就是聊聊Node.js中的程序、執行緒、協程與並行模型的詳細內容,更多請關注TW511.COM其它相關文章!