Node.js 現在、Node.js は、同時実行性の高いネットワーク アプリケーション サービスを構築するためのツールボックスのメンバーになりました。 ?この記事では、プロセス、スレッド、コルーチン、I/O モデルの基本概念から始めて、Node.js と同時実行モデルについて包括的に紹介します。
プロセス
一般に、プログラムの実行インスタンスをプロセスと呼びます。これは、オペレーティング システムによるリソースの割り当てとスケジューリングの基本単位です。
- プログラム: プロセスによって完了される関数を記述するために使用される、実行されるコード;
- データ領域: によって処理されるデータ空間プロセス (データ、動的に割り当てられたメモリ、処理関数のユーザー スタック、変更可能なプログラム、その他の情報を含む)
- プロセス テーブル項目: プロセス モデルを実装するために、オペレーティング システムは
process と呼ばれるプロセスを維持します。 table
テーブルでは、各プロセスは プロセス テーブル エントリ
( プロセス制御ブロック
とも呼ばれます) を占有します。これには、プログラム カウンター、スタック ポインター、メモリ割り当て、および開いているファイルのステータスが含まれます。スケジュール情報やその他の重要なプロセス ステータス情報を使用して、プロセスが一時停止された後にオペレーティング システムがプロセスを正しく復活できるようにします。
プロセスには次の特徴があります:
- 動的性: プロセスの本質は、マルチプログラミング システムにおけるプログラムの実行プロセスです。 ;
- 同時性: 任意のプロセスを他のプロセスと同時に実行できます;
- 独立性: プロセスは独立して実行できる基本単位であり、独立した単位でもあります;
- 非同期性: プロセス間の相互制約により、プロセスは断続的に実行されます。つまり、プロセスは独立した予測不可能な速度で前進します。
プログラムが 2 回実行されると、オペレーティング システムでコードの共有が可能になったとしても (つまり、コードのコピーが 1 つだけメモリ内にある)、プログラムは変更できないことに注意してください。実行中のプログラム 2 つのインスタンスが 2 つの異なるプロセスであるという事実。
プロセスの実行中、中断や CPU スケジューリングなどのさまざまな理由により、プロセスは次の状態の間で切り替わります:
# #実行状態: プロセスは現時点で実行中であり、CPU を占有しています; - 準備完了状態: プロセスは現時点で準備ができており、いつでも実行できますが、他のプロセスが実行中であるため一時的に停止しています;
- ブロック状態: プロセスは現在ブロック状態にあり、外部イベント (キーボード入力データの到着など) が発生しない限り、プロセスは実行できません。
-
上記のプロセス状態切り替え図からわかるように、プロセスは実行状態から準備完了状態およびブロッキング状態に切り替えることができますが、実行状態に直接切り替えることができるのは準備完了状態のみです。
実行状態から準備完了状態への切り替えは、システムが現在のプロセスが CPU 時間を占有しすぎていると判断し、他のプロセスに許可を与えることを決定するため、プロセス スケジューラによって引き起こされます。プロセスは CPU 時間を使用し、プロセス スケジューラはオペレーティング システムのものです 一部、プロセスはスケジューラの存在さえ感じません; - 実行状態からブロッキング状態への切り替えは、プロセスの独自の理由 (ユーザーのキーボード入力を待機しているなど) プロセスは実行を続行できず、ハングして何かを待つことしかできない イベント (キーボード入力データの到着など) が発生し、関連するイベントが発生すると、プロセスが最初に実行されます実行可能状態に変わります。この時点で他のプロセスが実行されていない場合は、すぐに実行中状態に変換されます。そうでない場合は、プロセスは実行可能状態のままでプロセスを待ちます。スケジューラによるスケジューリング。
-
スレッド
次の問題を解決するためにスレッドを使用する必要がある場合があります:
プロセス数が増加するにつれて, プロセス間の切り替えコストがますます高くなり、CPU の実効使用率が低下し、ひどい場合にはシステムがフリーズするなどの現象が発生する可能性があります; - 各プロセスには独自の独立したメモリ空間があり、各プロセス間のメモリ空間は互いに分離されており、一部のタスクは一部のデータを共有する必要があるため、複数のプロセス間のデータ同期は非常に面倒です。
-
スレッドに関しては、次の点を理解しておく必要があります。
スレッドは、プログラム実行における単一の連続した制御フローであり、オペレーティング システムが実行する最小単位です。計算のスケジューリングを実行できます。これはプロセスに含まれており、プロセス内の実際の実行単位です。- #プロセスには複数のスレッドを含めることができ、各スレッドは異なるタスクを並行して実行します。;
- Inプロセス すべてのスレッドは、プロセスのメモリ空間 (コード、データ、ヒープなどを含む) と一部のリソース情報 (開いているファイルやシステム シグナルなど) を共有します。
- あるプロセス内のスレッドは、他のプロセスには表示されません。 。
-
スレッドの基本的な特性を理解したところで、いくつかの一般的なスレッドの種類について説明しましょう。
カーネル状態スレッド
カーネル状態スレッドは、オペレーティング システムによって直接サポートされるスレッドであり、その主な機能は次のとおりです。
- スレッドの作成、スケジューリング、同期、および破棄はシステム カーネルによって完了しますが、そのオーバーヘッドは比較的高くなります。
- カーネルは、カーネル状態のスレッドを各プロセッサにマップできます。プロセッサ コアはカーネル スレッドに対応するため、CPU リソースを完全に競合して利用します。
- カーネルのコードとデータにのみアクセスできます。
- リソースの同期とデータの効率が向上します。データ共有は、プロセス リソースの同期やデータ共有の効率よりも低くなります。
#ユーザーモード スレッド
ユーザーモード スレッドは、完全にユーザー空間に構築されたスレッドであり、主な特徴は次のとおりです:
スレッドの作成、スケジューリング、同期、および破棄はユーザー空間によって完了され、そのオーバーヘッドは非常に低いです; - ユーザーモードのスレッドはユーザー空間によって維持されるため、カーネルはユーザーモードスレッドの存在をまったく認識しないため、カーネルのみが属するプロセスがスケジューリングとリソース割り当てを行い、プロセス内のスレッドのスケジューリングとリソース割り当てはプログラム自体によって処理されるため、次のような問題が発生する可能性があります。ユーザーモード スレッドがシステム コールでブロックされると、プロセス全体がブロックされます。
- すべての共有アドレス空間と、それが属するプロセスのシステム リソースにアクセスする機能。
- リソースの同期とデータ共有がより効率的になります。
-
軽量プロセス (LWP)
軽量プロセス (LWP) は、カーネル上に構築され、カーネルによってサポートされるユーザー スレッドです。主な機能は次のとおりです。
- ユーザー空間は、ユーザー状態スレッドとカーネル スレッド間のブリッジとみなすことができる軽量プロセス (LWP) を介してのみカーネル スレッドを使用できます。カーネル スレッドをサポートする軽量プロセス (LWP) はありますか。
- 軽量プロセス (LWP) のほとんどの操作では、システム コールを開始するためにユーザー モード スペースが必要です。このシステム コールのコストは比較的高い (ユーザー モードとカーネル モード間の切り替えが必要);
- 各軽量プロセス (LWP) は特定のカーネル スレッドに関連付ける必要があるため、次のようになります:
カーネル スレッドと同様に、システム全体で CPU リソースを完全に競合して利用できます。- 各軽量プロセス (LWP) は独立したスレッド スケジューリング ユニットであるため、軽量プロセス (LWP) がブロックされていても、システム コールでは、プロセス全体の実行には影響しません。
- 軽量プロセス (LWP) は、カーネル リソース (主にカーネル スレッドのスタック スペースを指します) を消費する必要があるため、システム内の多数の軽量プロセス (LWP) をサポートし、
-
- # は、それらが属するプロセスのすべての共有アドレス空間とシステムにアクセスできます。
概要
上記では、一般的なスレッド タイプ (カーネル状態スレッド、ユーザー状態スレッド、軽量プロセス) について簡単に説明しました。それぞれに独自の適用範囲があり、実際の使用においては、一般的な1対1、多対1、多対多など、ニーズに応じて自由に組み合わせて使用できます。スペースの制限があるため、この記事ではこれについてはあまり紹介しません。興味のある学生は自分で調べてください。
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- を返します。
この 2 つの主な違いは、実際の操作中の状態制御にあります。
関数
updateUserName- の実行中は、前述のように、上記の論理シーケンスは順番に実行されます;
関数 updateUserNameAsync- の実行中、関数は、
await## の場合を除き、上記の論理シーケンスに従って順番に実行されます。 # が発生した場合、
updateUserNameAsync は一時停止され、一時停止された場所に現在のプログラムの状態が保存されます。後続のプログラム フラグメントが
await に戻るまで、再び起動されることはありません。
そして、一時停止前のプログラムの状態に戻して、次のプログラムの実行を続けます。
上記の分析を通じて、大胆に推測できます。コルーチンが解決する必要があるのは、プロセスやスレッドが解決する必要があるプログラムの同時実行性の問題ではなく、非同期タスク (ファイルなど) を処理するときに遭遇する問題です。
async/await が登場する前は、非同期タスクはコールバック関数を通じてしか処理できず、簡単に
コールバック地獄
に陥り、混乱が生じる可能性がありました。一般に保守が難しいコードは、コルーチンを使用して非同期コードを同期することで実現できます。 <p>要牢記的是:協程的核心能力是能夠將某段程式掛起並維護程式掛起位置的狀態,並在未來某個時刻在掛起的位置恢復,並繼續執行掛起位置後的下一段程序。 </p>
<h2 data-id="heading-7"><strong>I/O 模型</strong></h2>
<p>一個完整的<code>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 中国語 Web サイトの他の関連記事を参照してください。