ホームページ >ウェブフロントエンド >jsチュートリアル >Node.js イベント ループの内部: 詳細
Node.js はイベント駆動型の非同期 I/O アプローチを採用し、シングルスレッドで同時実行性の高い JavaScript ランタイム環境を実現します。シングルスレッドは一度に 1 つのことしか実行できないことを意味します。Node.js はどのようにしてたった 1 つのスレッドで高い同時実行性と非同期 I/O を実現するのでしょうか?この記事では、この質問を中心に Node.js のシングルスレッド モデルについて説明します。
一般に、高い同時実行性を実現する解決策は、マルチスレッド モデルを提供することです。サーバーは各クライアント要求に 1 つのスレッドを割り当て、同期 I/O を使用します。システムは、スレッド切り替えを通じて同期 I/O 呼び出しの時間コストを補います。たとえば、Apache はこの戦略を使用します。 I/O 操作には通常時間がかかるため、このアプローチで高いパフォーマンスを達成するのは困難です。ただし、非常にシンプルなので、複雑な対話ロジックを実装できます。
実際、ほとんどの Web サーバー側はあまり計算を実行しません。リクエストを受信した後、そのリクエストを他のサービス (データベースの読み取りなど) に渡し、結果が返されるのを待ち、最後に結果をクライアントに送信します。したがって、Node.js はシングルスレッド モデルを使用してこの状況に対処します。受信リクエストごとにスレッドを割り当てる代わりに、メイン スレッドを使用してすべてのリクエストを処理し、I/O 操作を非同期で処理することで、スレッドの作成、破棄、およびスレッド間の切り替えに伴うオーバーヘッドと複雑さを回避します。
Node.js はメインスレッドでイベント キューを維持します。リクエストを受信すると、そのリクエストはイベントとしてこのキューに追加され、その後も引き続き他のリクエストを受信します。メインスレッドがアイドル状態 (リクエストが受信されていない状態) になると、イベント キューのループを開始して、処理するイベントがあるかどうかを確認します。 2 つのケースがあります。非 I/O タスクの場合、メインスレッドはそれらを直接処理し、コールバック関数を介して上位層に戻ります。 I/O タスクの場合、スレッド プールからスレッドを取得してイベントを処理し、コールバック関数を指定して、キュー内の他のイベントをループし続けます。
スレッド内の I/O タスクが完了すると、指定されたコールバック関数が実行され、完了したイベントがイベント キューの最後に配置され、イベント ループを待ちます。メインスレッドがこのイベントを再びループすると、それを直接処理して上位層に返します。このプロセスはイベント ループと呼ばれ、その動作原理は次の図に示されています。
この図は、Node.js の全体的な動作原理を示しています。 Node.js は、左から右、上から下の順に、アプリケーション層、V8 エンジン層、Node API 層、LIBUV 層の 4 つの層に分かれています。
Linux プラットフォームでも Windows プラットフォームでも、Node.js は内部でスレッド プールを使用して非同期 I/O 操作を完了し、LIBUV は異なるプラットフォームの呼び出しを統合します。したがって、Node.js のシングル スレッドは、JavaScript がシングル スレッドで実行されることを意味するだけであり、Node.js 全体がシングル スレッドであることを意味するわけではありません。
非同期を実現する Node.js の中核はイベントにあります。つまり、すべてのタスクをイベントとして扱い、イベント ループを通じて非同期効果をシミュレートします。この事実をより具体的かつ明確に理解して受け入れるために、疑似コードを使用してその動作原理を以下に説明します。
これはキューなので、先入れ先出し (FIFO) データ構造です。 JS 配列を使用して次のように記述します。
/** * Define the event queue * Enqueue: push() * Dequeue: shift() * Empty queue: length === 0 */ let globalEventQueue = [];
配列を使用してキュー構造をシミュレートします。配列の最初の要素はキューの先頭で、最後の要素はキューの末尾です。 Push() はキューの最後に要素を挿入し、shift() はキューの先頭から要素を削除します。このようにして、単純なイベントキューが実現されます。
以下に示すように、すべてのリクエストがインターセプトされ、処理関数に入ります。
/** * Receive user requests * Every request will enter this function * Pass parameters request and response */ function processHttpRequest(request, response) { // Define an event object let event = createEvent({ params: request.params, // Pass request parameters result: null, // Store request results callback: function() {} // Specify a callback function }); // Add the event to the end of the queue globalEventQueue.push(event); }
この関数は、ユーザーのリクエストをイベントとしてパッケージ化してキューに入れ、他のリクエストを引き続き受信します。
メインスレッドがアイドル状態になると、イベントキューのループが開始されます。したがって、イベント キューをループする関数を定義する必要があります。
/** * The main body of the event loop, executed by the main thread when appropriate * Loop through the event queue * Handle non-IO tasks * Handle IO tasks * Execute callbacks and return to the upper layer */ function eventLoop() { // If the queue is not empty, continue to loop while (this.globalEventQueue.length > 0) { // Take an event from the head of the queue let event = this.globalEventQueue.shift(); // If it's a time-consuming task if (isIOTask(event)) { // Take a thread from the thread pool let thread = getThreadFromThreadPool(); // Hand it over to the thread to handle thread.handleIOTask(event); } else { // After handling non-time-consuming tasks, directly return the result let result = handleEvent(event); // Finally, return to V8 through the callback function, and then V8 returns to the application event.callback.call(null, result); } } }
メインスレッドはイベントキューを継続的に監視します。 I/O タスクの場合はスレッド プールに渡して処理し、非 I/O タスクの場合は自身で処理して戻ります。
スレッド プールはタスクを受信した後、データベースの読み取りなどの I/O 操作を直接処理します。
/** * Define the event queue * Enqueue: push() * Dequeue: shift() * Empty queue: length === 0 */ let globalEventQueue = [];
I/O タスクが完了すると、コールバックが実行され、リクエスト結果がイベントに格納され、イベントがキューに戻されてループを待ちます。最後に、現在のスレッドが解放されます。メインスレッドがこのイベントを再度ループすると、直接処理されます。
上記のプロセスを要約すると、Node.js はリクエストの受信にメイン スレッドを 1 つだけ使用していることがわかります。リクエストを受信した後、それらを直接処理せずにイベント キューに入れ、引き続き他のリクエストを受信します。アイドル状態の場合、イベント ループを通じてこれらのイベントを処理し、非同期効果を実現します。もちろん、I/O タスクの場合は、システム レベルでスレッド プールに依存して処理する必要があります。
したがって、Node.js 自体はマルチスレッド プラットフォームですが、単一のスレッドで JavaScript レベルのタスクを処理するということが簡単に理解できます。
ここまでで、Node.js のシングルスレッド モデルについて簡単かつ明確に理解できたはずです。イベント駆動型モデルにより、高い同時実行性と非同期 I/O を実現します。ただし、Node.js が苦手な点もあります。
上で述べたように、I/O タスクの場合、Node.js はそれらをスレッド プールに渡して非同期処理を行います。これは効率的かつシンプルです。したがって、Node.js は I/O 集中型のタスクを処理するのに適しています。ただし、すべてのタスクが I/O 集中型であるわけではありません。 CPU を大量に使用するタスク、つまり、データの暗号化と復号化 (node.bcrypt.js)、データの圧縮と解凍 (node-tar) などの CPU 計算のみに依存する操作が発生した場合、Node.js はそれらを 1 つずつ処理します。 1つ。前のタスクが完了していない場合、後続のタスクは待つことしかできません。以下の図に示すように:
イベントキューでは、前の CPU 計算タスクが完了していないと、後続のタスクがブロックされ、応答が遅くなります。オペレーティング システムがシングルコアの場合は、許容できる可能性があります。しかし、現在、ほとんどのサーバーはマルチ CPU またはマルチコアであり、Node.js には EventLoop が 1 つしかありません。つまり、占有する CPU コアは 1 つだけです。 Node.js が CPU を集中的に使用するタスクによって占有され、他のタスクがブロックされると、CPU コアがアイドル状態のままになり、リソースが無駄になります。
したがって、Node.js は CPU を集中的に使用するタスクには適していません。
最後に、Node.js サービスのデプロイに最適なプラットフォームである Leapcell を紹介します。
ドキュメントでさらに詳しく見てみましょう!
Leapcell Twitter: https://x.com/LeapcellHQ
以上がNode.js イベント ループの内部: 詳細の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。