概要長年にわたり、Node.js は、主に JavaScript のシングルスレッドの性質により、CPU を集中的に使用するアプリケーションを実装するための最適な選択肢ではありませんでした。この問題の解決策として、Node.js v10.5.0 では、worker_threads モジュールを通じて「ワーカー スレッド」という実験的な概念が導入され、Node.js v12 LTS から安定した機能になりました。この記事では、その仕組みと、ワーカー スレッドを使用して最高のパフォーマンスを得る方法について説明します。 Node.js における CPU バウンド アプリケーションの歴史ワーカー スレッドが登場する前は、Node.js で CPU を集中的に使用するアプリケーションを実行する方法は複数ありました。いくつか例を挙げると:
しかし、パフォーマンスの制限、複雑さの増加、採用率の低さ、ドキュメントの不足などにより、これらのソリューションはいずれも広く採用されていません。 CPUを集中的に使用する操作にはワーカースレッドを使用するworker_threads は JavaScript の同時実行性の問題に対する優れたソリューションですが、JavaScript 自体にマルチスレッド機能をもたらすものではありません。対照的に、worker_threads は、Node によって提供されるワーカーと親ワーカー間の通信を使用して、複数の分離された JavaScript ワーカーを使用してアプリケーションを実行することで並行性を実現します。混乱していますか? ♂️ Node.js では、各ワーカーに独自の V8 インスタンスとイベント ループが存在します。しかし、child_process とは異なり、ワーカーはメモリを共有しません。 上記の概念については後で説明します。まず、ワーカー スレッドの使用方法を簡単に見てみましょう。単純な使用例は次のようになります。 // ワーカーシンプル.js const {Worker、isMainThread、parentPort、workerData} = require('worker_threads'); if (isMainThread) { const ワーカー = 新しいワーカー (__filename、{ワーカーデータ: {数値: 5}}); ワーカー.once('メッセージ', (結果) => { console.log('5の2乗は:', result); }) } それ以外 { 親ポート.postMessage(ワーカーデータ.num * ワーカーデータ.num) } 上記の例では、各ワーカーに数値を渡してその二乗値を計算しました。計算後、子ワーカーは結果をメインワーカースレッドに送り返します。一見シンプルに見えますが、Node.js を初めて使用する人にとっては少し混乱する可能性があります。 ワーカースレッドはどのように機能しますか?JavaScript 言語にはマルチスレッド機能がありません。したがって、Node.js ワーカー スレッドは、他の多くの高水準言語の従来のマルチスレッドとは異なる動作をします。 Node.js では、ワーカーの役割は、親ワーカーによって提供されるコード (ワーカー スクリプト) を実行することです。このワーカー スクリプトは他のワーカーとは独立して実行され、自身と親ワーカーの間でメッセージを渡すことができます。ワーカー スクリプトは、スタンドアロン ファイルでも、eval によって解析できるテキスト スクリプトでもかまいません。この場合、親ワーカー コードと子ワーカー コードの両方が同じスクリプト ファイルにあり、isMainThread プロパティによってその役割が決定されるため、__filename をワーカー スクリプトとして使用します。 各ワーカーは、メッセージ チャネルを介して親ワーカーに接続されます。子ワーカーは、parentPort.postMessage() 関数を使用してメッセージ チャネルにメッセージを書き込むことができ、親ワーカーはワーカー インスタンスで worker.postMessage() 関数を呼び出してメッセージ チャネルにメッセージを書き込みます。図1をご覧ください。 メッセージ チャネルは、「ポート」と呼ばれる 2 つの端を持つ単純な通信チャネルです。 JavaScript/NodeJS の用語では、メッセージ チャネルの両端は port1 と port2 と呼ばれます。 Node.js ワーカーはどのように並列動作するのでしょうか?ここで重要な疑問が浮かびます。JavaScript は並行性を直接提供しないので、2 つの Node.js ワーカーを並列に実行するにはどうすればよいでしょうか。答えはV8アイソレートです。 V8 アイソレートは、独自の JS スタックとマイクロタスク キューを備えた、Chrome V8 ランタイムの個別のインスタンスです。これにより、各 Node.js ワーカーは他のワーカーから完全に分離して JavaScript コードを実行できるようになります。欠点は、ワーカーが他のワーカーのヒープ データに直接アクセスできないことです。 さらに読む: JS はブラウザと Node でどのように機能しますか? したがって、各ワーカーは、親ワーカーや他のワーカーから独立した libuv イベント ループの独自のコピーを持つことになります。 JS/C++の境界を越える新しいワーカーのインスタンス化と親/兄弟 JS スクリプトとの通信の提供はすべて、ワーカーの C++ バージョンによって実行されます。執筆時点での実装はworker.cc (https://github.com/nodejs/node/blob/921493e228/src/node_worker.cc) です。 ワーカー実装は、worker_threads モジュールを介してユーザーレベルの JavaScript スクリプトとして公開されます。 JS 実装は 2 つのスクリプトに分割されており、次のように呼ばれます。
図 2 はこのプロセスをより明確に説明しています。 上記に基づいて、ワーカーのセットアップ プロセスを 2 つの段階に分けることができます。
各段階で何が起こるか見てみましょう。 初期化手順1. ユーザーレベルのスクリプトはworker_threadsを使用してワーカーインスタンスを作成します。 2. ノードの親ワーカー初期化スクリプトが C++ を呼び出して、空のワーカー オブジェクトを作成します。この時点では、作成されたワーカーは、まだ開始されていない単純な C++ オブジェクトです。 3. C++ワーカーオブジェクトが作成されると、スレッドIDを生成し、それを自身に割り当てる。 4. 同時に、親ワーカーによって空の初期化メッセージ チャネル (IMC と呼びます) が作成されます。これは、図 2 の灰色の「初期化メッセージ チャネル」セクションに示されています。 5. ワーカー初期化スクリプトによってパブリック JS メッセージ チャネル (PMC と呼ばれる) が作成されます。このチャネルは、ユーザーレベルの JS によって、親ワーカーと子ワーカー間でメッセージを渡すために使用されます。この部分は主に図 1 で説明されており、図 2 でも赤でマークされています。 6. ノード親ワーカー初期化スクリプトは C++ を呼び出し、ワーカー実行スクリプトに送信する必要がある初期メタデータを IMC に書き込みます。 初期メタデータとは何ですか?つまり、スクリプト名、ワーカー データ、PMC のポート 2、およびその他の情報など、ワーカーを起動するためにスクリプトが知っておく必要のあるデータです。 この例では、初期化メタデータは次のようになります。 :phone: やあ!ワーカーはスクリプトを実行します。ワーカー データ {num: 5} などを使用して、worker-simple.js を実行してください。ワーカーが PMC からデータを読み取れるように、PMC のポート 2 も渡してください。 次のスニペットは、初期化データが IMC に書き込まれる方法を示しています。 const kPublicPort = シンボル('kPublicPort'); // ... const { port1, port2 } = 新しい MessageChannel(); this[kPublicPort] = ポート1; this[kPublicPort].on('message', (message) => this.emit('message', message)); // ... this[kPort].postMessage({ タイプ: 'loadScript', ファイル名、 doEval: !!options.eval, cwdCounter: cwdCounter || workerIo.sharedCwdCounter、 ワーカーデータ: options.workerData、 パブリックポート: port2、 // ... 標準入力あり: !!options.stdin }, [ポート2]); コード内のこの[kPort]は、初期化スクリプト内のIMCのエンドポイントです。ワーカー初期化スクリプトは IMC にデータを書き込みますが、ワーカー実行スクリプトはそのデータにアクセスできません。 実行手順この時点で初期化は完了です。次に、ワーカー初期化スクリプトが C++ を呼び出してワーカー スレッドを開始します。 1. 新しい V8 アイソレートが作成され、ワーカーに割り当てられます。前述したように、「v8 isolate」は Chrome V8 ランタイムの別のインスタンスです。これにより、ワーカー スレッドの実行コンテキストがアプリケーション コードの残りの部分から分離されます。 2.libuvが初期化されます。これにより、ワーカー スレッドがアプリケーションの他の部分から独立して独自のイベント ループを維持することが保証されます。 3. ワーカー実行スクリプトが実行され、ワーカーのイベント ループが開始されます。 4. ワーカーは C++ を呼び出すスクリプトを実行し、IMC から初期化メタデータを読み取ります。 5. ワーカーはスクリプトを実行し、対応するファイルまたはコード (この場合は worker-simple.js) を実行して、ワーカーとして実行を開始します。 ワーカー実行スクリプトが IMC からデータを読み取る方法については、次のコード スニペットを参照してください。 const publicWorker = require('worker_threads'); // ... port.on('メッセージ', (メッセージ) => { メッセージタイプ === 'loadScript' の場合 { 定数{ cwdカウンタ、 ファイル名、 実行、 ワーカーデータ、 パブリックポート、 マニフェストSrc、 マニフェストURL、 標準入力を持つ } = メッセージ; // ... CJSLoader() を初期化します。 ESMLoader() を初期化します。 publicWorker.parentPort = publicPort; ワーカーデータを作成します。 // ... ポートの参照を解除します。 ポート.postMessage({ タイプ: UP_AND_RUNNING }); if (doEval) { const { evalScript } = require('internal/process/execution'); evalScript('[ワーカーeval]', ファイル名); } それ以外 { process.argv[1] = filename; // スクリプトファイル名 'module' が必要です。runMain(); } } // ... 上記のスニペットで、workerData プロパティと parentPort プロパティが publicWorker オブジェクトに割り当てられていることに気付きましたか?後者は、ワーカー実行スクリプトの require('worker_threads') によって導入されます。 そのため、workerData プロパティと parentPort プロパティは子ワーカー スレッド内でのみ使用でき、親ワーカーのコード内では使用できません。 親ワーカー コードでいずれかのプロパティにアクセスしようとすると、null が返されます。 ワーカースレッドを最大限に活用するNode.js ワーカー スレッドがどのように機能するかを理解したので、ワーカー スレッドを使用するときに最高のパフォーマンスを得るのに役立ちます。 worker-simple.js よりも複雑なアプリケーションを作成する場合、留意すべき主な考慮事項が 2 つあります。 ワーカー スレッドは実際のプロセスよりも軽量ですが、ワーカーを頻繁に重い作業に割り当てるとコストがかかる可能性があります。 並列 I/O 操作を処理するためにワーカー スレッドを使用することは、依然としてコスト効率が良くありません。これは、同じことを実行するワーカー スレッドを最初から開始するよりも、Node.js のネイティブ I/O メカニズムの方が高速な方法だからです。 ポイント 1 の問題を克服するには、「ワーカー スレッド プール」を実装する必要があります。 ワーカースレッドプールNode.js ワーカー スレッド プールは、実行中であり、後続のタスクで使用できるワーカー スレッドのセットです。新しいタスクが到着すると、親子メッセージ チャネルを介して利用可能なワーカーに渡すことができます。タスクが完了すると、子ワーカーは同じメッセージ チャネルを通じて結果を親ワーカーに返すことができます。 スレッド プールを適切に実装すると、新しいスレッドを作成するオーバーヘッドが削減され、パフォーマンスが大幅に向上します。また、効果的に実行できる並列スレッドの数は常にハードウェアによって制限されるため、膨大な数のスレッドを作成してもうまく機能する可能性は低いことにも注意してください。 次の図は、文字列を受信し、12 ラウンドのソルト処理を施した Bcrypt ハッシュを返す 3 つの Node.js サーバーのパフォーマンス比較です。 3 つのサーバーは次のとおりです。
一見すると、スレッド プールを使用すると、負荷が増大してもオーバーヘッドが大幅に減少することがわかります。 ただし、この記事の執筆時点では、スレッド プールは Node.js のネイティブ機能ではありません。したがって、サードパーティの実装に依存するか、独自のワーカー プールを作成する必要があります。 これで、ワーカー スレッドの仕組みについて十分に理解し、ワーカー スレッドを試して活用し、CPU 依存のアプリケーションを作成できるようになると思います。 上記は、Node.js のワーカースレッドを深く理解するための詳細な内容です。Node.js の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。 以下もご興味があるかもしれません:
|
>>: Linux の文字端末でマウスを使って赤い四角形を移動する方法
1. 背景1. vSphere の共有ストレージの背景を簡単に紹介するvSphere の重要な機能は...
CSS の 2D 変換を使用すると、移動、回転、拡大縮小、変形などの基本的な変換操作を 2 次元空間...
目次1. 一意の値をフィルタリングする2. 短絡評価2.1 シナリオ例3. ブール変換4. 文字列を...
前回の記事はBootstrap CSS部分の簡単なレビューであり、多くの詳細が見落とされていました。...
IE は開発の初期段階では頭を悩ませましたが、他のブラウザとは異なります。他のブラウザがサポートして...
一般的に言えば、コンテナが起動した後、ポート マッピングを通じてコンテナが提供するサービスを使用...
概要いずれかのデータベースに対する操作は他のデータベースに自動的に適用され、2 つのデータベースのデ...
タイトルの通り、ページを修正すると以下のような状況が発生する可能性があります。現在、古いページを改修...
Nginx によるソケット ポート転送の一般的なシナリオ: オンライン学習アプリケーションでは、通常...
MySQLサービスを停止するWindowsでは、マイコンピュータを右クリック--管理--サービスと...
当銀行のMGRは年末に開始されます。公式文書を読んだり、毎日テストを受けたりしなければなりません。毎...
HTMLを学ぶとき、画像タグ<img>は画像を導入します <img src=&qu...
docker イメージ ID は一意であり、イメージを物理的に識別できます。repository: ...
目次1. 需要2. 実装3. 結果1. 需要入力ボックスにデータを入力し、入力結果に基づいてデータベ...
アイデア: 最初にランダムに並べ替えてからグループ化します。 1. テーブルを作成します。 テーブル...