Node.js のワーカー スレッドの詳細な理解

Node.js のワーカー スレッドの詳細な理解

概要

長年にわたり、Node.js は、主に JavaScript のシングルスレッドの性質により、CPU を集中的に使用するアプリケーションを実装するための最適な選択肢ではありませんでした。この問題の解決策として、Node.js v10.5.0 では、worker_threads モジュールを通じて「ワーカー スレッド」という実験的な概念が導入され、Node.js v12 LTS から安定した機能になりました。この記事では、その仕組みと、ワーカー スレッドを使用して最高のパフォーマンスを得る方法について説明します。

Node.js における CPU バウンド アプリケーションの歴史

ワーカー スレッドが登場する前は、Node.js で CPU を集中的に使用するアプリケーションを実行する方法は複数ありました。いくつか例を挙げると:

  • child_processモジュールを使用して、子プロセスでCPUを集中的に使用するコードを実行します。
  • クラスタモジュールを使用して、複数のプロセスで複数のCPU集中型操作を実行します。
  • MicrosoftのNapa.jsなどのサードパーティモジュールの使用

しかし、パフォーマンスの制限、複雑さの増加、採用率の低さ、ドキュメントの不足などにより、これらのソリューションはいずれも広く採用されていません。

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 つのスクリプトに分割されており、次のように呼ばれます。

  • 初期化スクリプト worker.js — ワーカー インスタンスを初期化し、親ワーカーと子ワーカー間の初期通信を確立して、ワーカー メタデータが親ワーカーから子ワーカーに渡されるようにします。 (https://github.com/nodejs/node/blob/921493e228/lib/internal/worker.js)
  • スクリプト worker_thread.js を実行します。ユーザーによって提供された workerData データと親ワーカーによって提供されたその他のメタデータに従って、ユーザーのワーカー JS スクリプトを実行します。 (https://github.com/nodejs/node/blob/921493e228/lib/internal/main/worker_thread.js)

図 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 つのサーバーは次のとおりです。

  • マルチスレッドなし
  • マルチスレッド、スレッドプールなし
  • 4つのスレッドを持つスレッドプール

一見すると、スレッド プールを使用すると、負荷が増大してもオーバーヘッドが大幅に減少することがわかります。

ただし、この記事の執筆時点では、スレッド プールは Node.js のネイティブ機能ではありません。したがって、サードパーティの実装に依存するか、独自のワーカー プールを作成する必要があります。

これで、ワーカー スレッドの仕組みについて十分に理解し、ワーカー スレッドを試して活用し、CPU 依存のアプリケーションを作成できるようになると思います。

上記は、Node.js のワーカースレッドを深く理解するための詳細な内容です。Node.js の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • プロセス解析を使用する Javascript Web Worker
  • Yii2 と Workerman の Websocket の例を組み合わせた詳細な説明
  • JavaScript での Web ワーカー マルチスレッド API の研究
  • 複数ページ通信を実現する JavaScript の sharedWorker の詳細な例
  • nodejs で worker_threads を使用して新しいスレッドを作成する方法
  • Javascript ワーカー サブスレッド コード例
  • JavaScript でのワーカー イベント API の理解
  • JS で webWorker を使用する方法

<<:  MySQLの文字セット設定を5分で理解しましょう

>>:  Linux の文字端末でマウスを使って赤い四角形を移動する方法

推薦する

JS正規RegExpオブジェクトについての簡単な説明

目次1. RegExpオブジェクト2. 文法2.1 定義2.2 修飾子2.3 角括弧2.4 メタ文字...

Sitemesh チュートリアル - ページ装飾技術の原理と応用

1. 基本概念1. Sitemeshはページ装飾技術です。 1 : フィルターを通してページアクセス...

win2008R2 64 ビット システムでの mysql5.7.17 のインストールと構成の例

123WORDPRESS.COM では、さまざまな環境での MYSQL の他のバージョンのインストー...

docker redis5.0 clusterの実装 クラスタ構築

システム環境: Ubuntu 16.04LTSこの記事では、6 つの Docker コンテナを使用し...

Docker Hubの動作原理と実装プロセスの分析

GitHub が提供するコード ホスティング サービスと同様に、Docker Hub はイメージ ホ...

テキストエリアの残りの単語数を動的に取得する方法

仕事で、これまで一度も書いたことのないケースに遭遇しました。午後の半分をその作業に費やし、ついに書き...

Zen Coding 簡単で素早いHTMLの書き方

禅コーディングテキストエディタプラグインです。 Zen Coding を使用するテキスト エディター...

このポイントのJavaScriptの基本

目次これ方法オブジェクト内これを隠した厳密モード要約するJavaScript の this も不思議...

レスポンシブ Web をデザインするにはどうすればいいですか?レスポンシブウェブデザインのメリットとデメリット

最近レスポンシブ デザインについて学んでいて、これについていくつか整理してみました。写真の一部はイン...

Vueプロジェクトをパッケージ化してリリースする手順

目次1. 開発環境から本番環境への移行2. 統一されたリクエストパスを設定する3. パッケージ化コマ...

SeataがMySQL 8バージョンを使用できない問題を解決する方法

考えられる理由: Seata が MySQL 8 をサポートしない主な理由は、接続ドライバーがバージ...

Linux 環境で crontab コマンドを使用して、スケジュールされた定期的な実行タスクを設定します (PHP 実行コードを含む)

この記事では、Linux 環境で crontab コマンドを使用して、タスクの定期的な実行をスケジュ...

Vueコンポーネント通信のさまざまな方法の詳細な説明

目次1. 父から息子へ2. 息子から父へ3. 親子関係のないコンポーネントの値の転送4. ヴュークス...

Js でオブジェクトのディープ オブジェクトを安全に取得するメソッドの例

目次序文文章パラメータ例Lodash 実装:トーキー機能: castPath関数: stringTo...

Nodejs で WeChat アカウント分割を実装するためのサンプルコード

会社のビジネスシナリオでは、WeChat アカウント分割機能を使用する必要があります。公式 Web ...