あなたがレストランに行って、「私は同時に何百人もの料理を作ることができます、そしてあなた方の誰もお腹を空かせることはありません」と約束する一人のシェフがいると仮定します。不可能に聞こえますよね?この 1 つのチェックを、複数の注文をすべて管理し、すべての顧客に料理を提供する Node JS と考えることができます。
誰かに「Node JS とは何ですか?」という質問をすると、必ず「Node JS はブラウザ環境の外で JavaScript を実行するために使用されるランタイムです」という答えが返ってきます。
しかし、ランタイムとは何を意味するのでしょうか?... ランタイム環境は、コードの実行が特定のプログラミング言語で記述されるソフトウェア インフラストラクチャです。コードの実行、エラーの処理、メモリの管理を行うためのすべてのツール、ライブラリ、機能が備わっており、基盤となるオペレーティング システムやハードウェアと対話できます。
Node JS にはこれらがすべて含まれています。
コードを実行するには Google V8 エンジン。
fs、crypto、http などのコア ライブラリと API。
非同期およびノンブロッキング I/O 操作をサポートする Libuv やイベント ループなどのインフラストラクチャ。
これで、Node JS がランタイムと呼ばれる理由がわかりました。
このランタイムは、V8 と libuv という 2 つの独立した依存関係で構成されます。
V8 は Google Chrome でも使用されているエンジンで、Google が開発・管理しています。 Node JS では JavaScript コードを実行します。コマンド node Index.js を実行すると、Node JS はこのコードを V8 エンジンに渡します。 V8 はこのコードを処理して実行し、結果を提供します。たとえば、コードが「Hello, World!」をログに記録するとします。コンソールに対して、V8 はこれを実現する実際の実行を処理します。
libuv ライブラリには、ネットワーク、I/O 操作、または時間関連の操作などの機能が必要な場合にオペレーティング システムへのアクセスを可能にする C コードが含まれています。これは、Node JS とオペレーティング システムの間のブリッジとして機能します。
libuv は次の操作を処理します:
ファイル システム操作: ファイルの読み取りまたは書き込み (fs.readFile、fs.writeFile)。
ネットワーク: HTTP リクエスト、ソケットの処理、またはサーバーへの接続。
タイマー: setTimeout や setInterval などの関数を管理します。
ファイル読み取りなどのタスクは Libuv スレッド プールによって処理され、タイマーは Libuv のタイマー システムによって処理され、ネットワーク呼び出しは OS レベルの API によって処理されます。
次の例を見てください。
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 4; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
同じファイルを 4 回読み取り、それらのファイルを読み取る時間を記録しています。
このコードから次の出力が得られます。
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
ほぼ 50 ミリ秒で 4 つのファイルすべての読み取りが完了したことがわかります。 Node JS がシングルスレッドの場合、これらすべてのファイルの読み取り操作はどのようにして同時に完了するのでしょうか?
この質問は、libuv ライブラリがスレッド プールを使用していると答えています。スレッド プールはスレッドの束です。デフォルトでは、スレッド プール サイズは 4 で、libuv によって一度に 4 つのリクエストを処理できることを意味します。
1 つのファイルを 4 回読み取る代わりに、このファイルを 6 回読み取る別のシナリオを考えてみましょう。
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 4; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
出力は次のようになります:
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
読み取り操作 1 と 2 が完了し、スレッド 1 と 2 が解放されたとします。
最初の 4 回はファイルの読み取りにほぼ同じ時間がかかりますが、このファイルを 5 回目と 6 回目に読み取ると、読み取り操作を完了するまでに最初の 4 回の読み取り操作に比べてほぼ 2 倍の時間がかかることがわかります。 。
これは、スレッド プール サイズがデフォルトで 4 であるため、4 つの読み取り操作が同時に処理されますが、ファイルの読み取りが 2 回 (5 回目と 6 回目) 行われるため、すべてのスレッドが何らかの作業を行っているため、libuv が待機するために発生します。 4 つのスレッドのうち 1 つが実行を完了すると、5 回目の読み取り操作がそのスレッドに対して処理され、6 回目の読み取り操作も同様に実行されます。それがより時間がかかる理由です。
つまり、Node JS はシングルスレッドではありません。
しかし、なぜ一部の人はそれをシングルスレッドと呼ぶのでしょうか?
これは、メイン イベント ループがシングルスレッドであるためです。このスレッドは、非同期コールバックの処理やタスクの調整など、Node JS コードの実行を担当します。ファイル I/O などのブロック操作は直接処理しません。
コード実行の流れはこんな感じです。
Node.js は、V8 JavaScript エンジンを使用して、すべての同期 (ブロッキング) コードを 1 行ずつ実行します。
fs.readFile、setTimeout、http リクエストなどの非同期操作は、Libuv ライブラリまたは他のサブシステム (OS など) に送信されます。
ファイル読み取りなどのタスクは Libuv スレッド プールによって処理され、タイマーは Libuv のタイマー システムによって処理され、ネットワーク呼び出しは OS レベルの API によって処理されます。
非同期タスクが完了すると、それに関連付けられたコールバックがイベント ループのキューに送信されます。
イベント ループはキューからコールバックを取得して 1 つずつ実行し、ノンブロッキングな実行を保証します。
スレッド プールのサイズは、process.env.UV_THREADPOOL_SIZE = 8 を使用して変更できます。
今、スレッド数を高く設定すれば、大量のリクエストにも対応できるのではないかと考えています。皆さんも私と同じように考えていただければ幸いです。
しかし、それは私たちが考えていたこととは逆です。
スレッド数を特定の制限を超えて増やすと、コードの実行が遅くなります。
次の例を見てください。
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 4; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
出力:
高スレッド プール サイズ (100 スレッド)
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
次に、スレッド プール サイズを 4 (デフォルト サイズ) に設定した場合の出力を示します。
デフォルトのスレッド プール サイズ (4 スレッド)
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 6; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
合計実行時間には 100ms の差があることがわかります。合計実行時間 (スレッド プール サイズ 4) は 600 ミリ秒、合計実行時間 (スレッド プール サイズ 100) は 700 ミリ秒です。したがって、スレッド プール サイズが 4 の場合、所要時間は短くなります。
スレッドの数が多い != より多くのタスクを同時に処理できるのはなぜですか?
最初の理由は、各スレッドに独自のスタックとリソース要件があることです。スレッドの数を増やすと、最終的にはメモリ不足または CPU リソースの状態が発生します。
2 番目の理由は、オペレーティング システムがスレッドをスケジュールする必要があることです。スレッドが多すぎると、OS はスレッド間の切り替え (コンテキスト切り替え) に多くの時間を費やし、オーバーヘッドが追加され、パフォーマンスが向上するどころかパフォーマンスが低下します。
スケーラビリティと高いパフォーマンスを実現するためにスレッド プール サイズを増やすことではなく、クラスタリングなどの適切なアーキテクチャを使用し、タスクの性質 (I/O 対 CPU バウンド) を理解することが重要であると言えます。 ) と Node.js のイベント駆動モデルがどのように機能するか。
お読みいただきありがとうございます。
以上がノードJS内部の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。