Node.jsでのマルチスレッディング
Min-jun Kim
Dev Intern · Leapcell

Node.jsでは、シングルスレッドの性質上、メインスレッドはノンブロッキングI/O操作の実行に使用されます。ただし、CPU負荷の高いタスクを実行する場合、単一のスレッドだけに依存すると、パフォーマンスのボトルネックにつながる可能性があります。幸いなことに、Node.jsにはスレッドを有効化および管理するためのいくつかの方法が用意されており、アプリケーションはマルチコアCPUを活用できます。
なぜサブスレッドを有効にするのか?
Node.jsでサブスレッドを有効にする主な理由は、並行タスクを処理し、アプリケーションのパフォーマンスを向上させるためです。Node.jsは本質的にイベントループ、シングルスレッドモデルに基づいているため、すべてのI/O操作(ファイルの読み取り/書き込みやネットワークリクエストなど)はノンブロッキングです。ただし、CPU負荷の高いタスク(大規模な計算など)はイベントループをブロックし、アプリケーション全体のパフォーマンスに影響を与える可能性があります。
サブスレッドを有効にすると、次の問題の解決に役立ちます。
- ノンブロッキング操作:Node.jsの設計思想は、ノンブロッキングI/Oを中心に展開しています。ただし、外部コマンドがメインスレッドで直接実行される場合、実行プロセスがメインスレッドをブロックし、アプリケーションの応答性に影響を与える可能性があります。これらのコマンドをサブスレッドで実行することにより、メインスレッドはそのノンブロッキング特性を維持し、他の並行操作が影響を受けないようにします。
- システムリソースの効率的な使用:子プロセスまたはワーカースレッドを使用することにより、Node.jsアプリケーションはマルチコアCPUの計算能力をより有効に活用できます。これは、CPU負荷の高い外部コマンドを実行する場合に特に役立ちます。これらのコマンドは、Node.jsのメインイベントループに影響を与えることなく、個別のCPUコアで実行できます。
- 分離とセキュリティ:サブスレッドで外部コマンドを実行すると、アプリケーションにセキュリティの追加レイヤーが追加されます。外部コマンドが失敗またはクラッシュした場合、この分離はメインのNode.jsプロセスが影響を受けるのを防ぎ、アプリケーションの安定性を向上させます。
- 柔軟なデータ処理と通信:サブスレッドを使用すると、外部コマンドの出力を、メインプロセスに渡す前に柔軟に処理できます。Node.jsは、プロセス間通信(IPC)を実装するための複数の方法を提供し、シームレスなデータ交換を可能にします。
サブスレッドを有効にする方法
次に、Node.jsでサブスレッドを有効にするさまざまな方法について説明します。
子プロセス
Node.jsのchild_process
モジュールを使用すると、メインプロセスと通信できる子プロセスを作成して、システムコマンドまたはその他のプログラムを実行できます。これは、CPU負荷の高いタスクを実行したり、他のアプリケーションを実行したりするのに役立ちます。
spawn()
child_process
モジュールのspawn()
メソッドは、指定されたコマンドを実行する新しい子プロセスを作成するために使用されます。これは、stdout
およびstderr
ストリームを持つオブジェクトを返し、子プロセスとの対話を可能にします。このメソッドは、データを一度にすべてバッファリングするのではなく、ストリームとして処理するため、大量の出力を生成する長時間実行プロセスに最適です。
spawn()
関数の基本的な構文は次のとおりです。
const { spawn } = require('child_process'); const child = spawn(command, [args], [options]);
command
:実行するコマンドを表す文字列。args
:すべてのコマンドライン引数をリストする文字列の配列。options
:子プロセスの作成方法を構成するオプションのオブジェクト。一般的なオプションは次のとおりです。cwd
:子プロセスの作業ディレクトリ。env
:環境変数を含むオブジェクト。stdio
:子プロセスの標準入出力を構成します。多くの場合、パイプ操作またはファイルリダイレクトに使用されます。shell
:true
の場合、シェルでコマンドを実行します。デフォルトのシェルは、Unixでは/bin/sh
、Windowsではcmd.exe
です。detached
:true
の場合、子プロセスは親プロセスとは独立して実行され、親が終了した後も実行を継続できます。
spawn()
を使用した簡単な例を次に示します。
const { spawn } = require('child_process'); const path = require('path'); // 'touch'コマンドを使用して、'moment.txt'という名前のファイルを作成します const touch = spawn('touch', ['moment.txt'], { cwd: path.join(process.cwd(), './m'), }); touch.on('close', (code) => { if (code === 0) { console.log('File created successfully'); } else { console.error(`Error creating file, exit code: ${code}`); } });
このコードの目的は、現在の作業ディレクトリのm
サブディレクトリにmoment.txt
という名前の空のファイルを作成することです。成功した場合、成功メッセージが出力されます。それ以外の場合は、エラーメッセージが表示されます。
exec()
child_process
モジュールのexec()
メソッドは、指定されたコマンドを実行するために新しい子プロセスを作成するために使用され、生成された出力をバッファリングします。spawn()
とは異なり、exec()
は、出力が小さいシナリオに適しています。これは、子プロセスのstdout
とstderr
をメモリに格納するためです。
exec()
の基本的な構文は次のとおりです。
const { exec } = require('child_process'); exec(command, [options], callback);
command
:文字列として実行されるコマンド。options
:実行環境をカスタマイズするためのオプションのパラメーター。callback
:(error, stdout, stderr)
を引数として受け取るコールバック関数。
options
オブジェクトには、次のものを含めることができます。
cwd
:子プロセスの作業ディレクトリを設定します。env
:環境変数オブジェクトを指定します。encoding
:文字エンコーディング。shell
:実行に使用されるシェルを指定します(Unixでは/bin/sh
、Windowsではcmd.exe
)。timeout
:タイムアウトをミリ秒単位で設定します。実行がこの時間を超えると、子プロセスは強制終了されます。maxBuffer
:stdout
およびstderr
の最大バッファーサイズを設定します(デフォルト:1024 * 1024
または1MB)。killSignal
:プロセスを終了するために使用されるシグナルを定義します(デフォルト:'SIGTERM'
)。
コールバック関数は以下を受け取ります。
error
:コマンドの実行が失敗した場合、またはゼロ以外の終了コードを返した場合のError
オブジェクト。それ以外の場合はnull
。stdout
:コマンドの標準出力。stderr
:標準エラー出力。
exec()
を使用した例を次に示します。
const { exec } = require('child_process'); const path = require('path'); // ファイルパスを含む、実行するコマンドを定義します const command = `touch ${path.join('./m', 'moment.txt')}`; exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => { if (error) { console.error(`Error executing command: ${error}`); return; } if (stderr) { console.error(`Standard error output: ${stderr}`); return; } console.log('File created successfully'); });
このコードを実行すると、ファイルが作成され、適切な出力が表示されます。
fork()
child_process
モジュールのfork()
メソッドは、プロセス間通信(IPC)チャネルを介して親プロセスと通信する新しいNode.jsプロセスを作成するための特殊な方法です。fork()
は、Node.jsモジュールを個別に実行する場合に特に役立ち、マルチコアCPUでの並列実行に役立ちます。
fork()
の基本的な構文は次のとおりです。
const { fork } = require('child_process'); const child = fork(modulePath, [args], [options]);
modulePath
:子プロセスで実行するモジュールのパスを表す文字列。args
:モジュールに渡す引数を含む文字列の配列。options
:子プロセスを構成するオプションのオブジェクト。
options
オブジェクトには、次のものを含めることができます。
cwd
:子プロセスの作業ディレクトリ。env
:環境変数を含むオブジェクト。execPath
:子プロセスの作成に使用されるNode.js実行可能ファイルへのパス。execArgv
:Node.js実行可能ファイルに渡される引数のリストですが、モジュール自体には渡されません。silent
:true
の場合、子のstdin
、stdout
、およびstderr
を親プロセスにリダイレクトします。それ以外の場合は、親から継承します。stdio
:標準入出力ストリームを構成します。ipc
:親プロセスと子プロセス間の通信用のIPCチャネルを作成します。
fork()
を使用して作成された子プロセスは、IPCチャネルを自動的に確立し、親プロセスと子プロセス間のメッセージの受け渡しを可能にします。親はchild.send(message)
を使用してメッセージを送信でき、子プロセスはprocess.on('message', callback)
を使用してこれらのメッセージをリッスンできます。同様に、子プロセスはprocess.send(message)
を使用して親にメッセージを送信できます。
fork()
を使用して子プロセスを作成し、IPCを介して通信する方法を示す例を次に示します。
index.js
(親プロセス)
const { fork } = require('child_process'); const child = fork('./child.js'); child.on('message', (message) => { console.log('Message from child process:', message); }); child.send({ hello: 'world' }); setInterval(() => { child.send({ hello: 'world' }); }, 1000);
child.js
(子プロセス)
process.on('message', (message) => { console.log('Message from parent process:', message); }); process.send({ foo: 'bar' }); setInterval(() => { process.send({ hello: 'world' }); }, 1000);
この例では、親プロセス(index.js
)はchild.js
を実行する子プロセスを作成します。親プロセスは子にメッセージを送信し、子はそれを受信してログに記録し、応答を返します。親は子から受信したメッセージもログに記録します。タイマーは、定期的なメッセージ交換を保証します。
fork()
を使用すると、各子プロセスは独自のV8エンジンとイベントループを備えた個別のNode.jsインスタンスとして実行されます。これは、あまりにも多くの子プロセスを作成すると、リソースの消費量が多くなる可能性があることを意味します。
ワーカースレッド
Node.jsのworker_threads
モジュールは、単一のプロセス内で複数のJavaScriptタスクを並行して実行するためのメカニズムを提供します。これにより、アプリケーションは、複数のプロセスを生成せずに、特にCPU負荷の高いタスクに対して、マルチコアCPUリソースを最大限に活用できます。worker_threads
を使用すると、パフォーマンスが大幅に向上し、複雑な計算が可能になります。
ワーカースレッドの主な概念:
- Worker(ワーカー):JavaScriptコードを実行する独立したスレッド。各ワーカーは、独自のV8インスタンス、独自のイベントループ、およびローカル変数で実行されます。つまり、メインスレッドまたは他のワーカーとは独立して動作できます。
- メインスレッド:ワーカーを開始するスレッド。一般的なNode.jsアプリケーションでは、初期JavaScript実行環境(イベントループ)はメインスレッドで実行されます。
- 通信:メインスレッドとワーカーは、メッセージを渡すことによって通信します。
ArrayBuffer
やその他の転送可能なオブジェクトを含むJavaScript値を送信できるため、効率的なデータ転送が可能です。
ワーカーを作成し、メインスレッドとワーカー間で通信する方法を示す基本的な例を次に示します。
const { Worker, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { // メインスレッド const worker = new Worker(__filename); worker.on('message', (message) => { console.log('Message from Worker:', message); }); worker.postMessage('Hello Worker!'); } else { // ワーカースレッド parentPort.on('message', (message) => { console.log('Message from main thread:', message); parentPort.postMessage('Hello Main Thread!'); }); }
この例では、index.js
ファイルは、メインスレッドのエントリポイントとワカースクリプトの両方として機能します。isMainThread
を確認することにより、スクリプトはメインスレッドで実行されているか、ワーカーとして実行されているかを判断します。メインスレッドは、同じスクリプトを実行するワーカーを作成し、ワーカーにメッセージを送信します。ワーカーはpostMessage()
を介して応答を返します。
worker_threads
とfork()
の違い
概念:
worker_threads
:ワーカースレッドを使用して、同じプロセス内でJavaScriptコードを並行して実行します。fork()
:個別のNode.jsプロセスを生成します。各プロセスには、独自のV8インスタンスとイベントループがあります。
通信:
worker_threads
:MessagePort
を使用して、ArrayBuffer
およびMessageChannel
を含むJavaScript値を転送します。fork()
:process.send()
およびmessage
イベントを介してIPC(プロセス間通信)を使用します。
メモリ使用量:
worker_threads
:メモリを共有し、冗長なデータコピーを削減し、パフォーマンスを向上させます。fork()
:フォークされた各プロセスには、個別のメモリスペースと独自のV8インスタンスがあるため、メモリ使用量が多くなります。
最適な使用例:
worker_threads
:CPU負荷の高い計算と並列処理に適しています。fork()
:独立したNode.jsアプリケーションまたは分離されたサービスの実行に適しています。
全体として、worker_threads
またはfork()
のどちらを使用するかは、アプリケーションのニーズによって異なります。厳密なプロセス分離が必要な場合は、fork()
の方が適している場合があります。ただし、効率的な並列計算とデータ処理が必要な場合は、worker_threads
の方がパフォーマンスとリソース使用率が向上します。
Cluster(クラスタリング)
Node.jsのcluster
モジュールを使用すると、同じサーバーポートを共有する子プロセスを作成できます。これにより、Node.jsアプリケーションは複数のCPUコアで実行できるようになり、パフォーマンスとスループットが向上します。Node.jsはシングルスレッドであるため、そのノンブロッキングI/O操作は、多くの同時接続を処理するのに適しています。ただし、CPU負荷の高いタスクの場合、またはワークロードを複数のコアに分散する場合は、cluster
モジュールを使用すると特に役立ちます。
cluster
モジュールの基本的な動作原理は、マスタープロセス(多くの場合「マスター」と呼ばれます)が複数のワーカープロセスを作成できるようにすることです。ワーカープロセスは、基本的にメインプロセスのコピーです。マスタープロセスはこれらのワーカーを管理し、着信ネットワーク接続をそれらに分散します。
内部的には、cluster
モジュールはchild_process.fork()
を使用してワーカープロセスを作成します。つまり、各ワーカーは同じアプリケーションコードを実行します。主な違いは、IPC(プロセス間通信)を介してマスタープロセスと通信できることです。
cluster
モジュールを使用した簡単な例を次に示します。
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master process ${process.pid} is running`); // ワーカープロセスをフォークします for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`Worker process ${worker.process.pid} exited`); }); } else { // ワーカープロセスは、どのTCP接続でも共有できます // この例では、HTTPサーバーを作成します http .createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }) .listen(8000); console.log(`Worker process ${process.pid} started`); }
このスクリプトを実行してサーバーにリクエストを行うと、ログに異なるプロセスIDが表示され、複数のワーカープロセスがリクエストを処理していることが示されます。
この例では、マスタープロセスはCPUコアの数と同じ数のワーカープロセスを作成します。各ワーカープロセスは独立して実行され、着信HTTPリクエストを処理します。ワーカープロセスが終了すると、マスタープロセスはexit
イベントを介して通知されます。
cluster
モジュールはパフォーマンスと信頼性を向上させますが、ワーカーのライフサイクルの管理やプロセス間通信の処理など、複雑さも増します。場合によっては、プロセスマネージャー(pm2
など)を使用するなどの代替ソリューションがより適している場合があります。
ただし、cluster
モジュールはすべてのアプリケーションに必要なわけではありません。CPU負荷が高くないアプリケーションの場合、単一のNode.jsインスタンスで十分なすべてのワークロードを処理できる場合があります。
まとめ
子プロセスを使用すると、Node.jsアプリケーションはオペレーティングシステムのコマンドを実行したり、独立したNode.jsモジュールを実行したりして、並行処理を改善できます。exec()
、spawn()
、fork()
などのAPIを使用すると、開発者は子プロセスを柔軟に作成および管理でき、複雑な非同期およびノンブロッキング操作が可能になります。これにより、アプリケーションはメインイベントループを妨げることなく、システムリソースとマルチコアCPUの利点を最大限に活用できます。
子プロセス、ワーカースレッド、またはクラスタリングのいずれかの適切なスレッド処理方法を選択することで、パフォーマンスとスケーラビリティの両方でNode.jsアプリケーションを最適化できます。
Node.jsプロジェクトのホスティングに最適なLeapcellはこちらです。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ料金が発生します - リクエストも料金もかかりません。
比類のないコスト効率
- アイドル料金のない従量課金制。
- 例:25ドルで、平均応答時間60msで694万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察を得るためのリアルタイムのメトリクスとロギング。
簡単なスケーラビリティと高いパフォーマンス
- 簡単な自動スケーリングで、高い同時実行性を処理します。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください: @LeapcellHQ