Node.jsでもマルチスレッドプログラミングができるworker_threadsというモジュールがあります。
- worker_threadsの概要はこちらを参照→ Node.js: CPU負荷で3秒かかっていた処理を「Worker Threads」で1秒に時短する - Qiita
語弊がありますが似たようなモジュールにchild_processというものもあります。本稿では、worker_threadsとchild_processをワーカー間通信の速さという観点で比較していきます。
本稿でわかること
- child_processとworker_threadsどっちの通信速度のほうが速いか?
child_processは古参モジュールとして、マルチコアでの分散処理を支えてきた
worker_threadsは比較的新しいモジュールで、スレッドがNode.jsに導入される以前は、マルチコア環境のリソースを活かすには、Node.jsでは複数のプロセスを起動して負荷分散するというアプローチが取られてきました。Node.jsでマルチプロセス型の分散処理をするためによく使われるのが、child_processやclusterといったモジュールです。
worker_threadsもchild_processも似たようなワーカー間通信ができる
worker_threadsもchild_processも、処理をフォークして並列処理できるだけでなく、親ワーカーと子ワーカーの通信ができます。用語が多少異なりますが、worker_threadsの場合は、親スレッドと子スレッドとの間でデータを送受信できます。child_processの場合も、親プロセスと子プロセスの間でデータの送受信が可能です。
worker_threadsで、親スレッドから子スレッドにデータを送信する例:
const {Worker} = require('worker_threads')
const worker = new Worker('./worker.js')
worker.postMessage('Hello!')
child_processで、親プロセスから子プロセスにデータを送信する例:
const {fork} = require('child_process')
const childProcess = fork('./worker.js')
childProcess.send('Hello!')
どちらも似たようなワーカー間通信ができるのがコードからも分かるかと思います。
ちなみに、worker_threadstとchild_processの2つは、そもそもアーキテクチャが異なるので、通信方法もデータのシリアライズ方法は異なります。そのへんの違いについては、簡単な比較表を載せておきます:
どちらのワーカー間通信のほうが速い?
マルチコアを生かした分散処理をしようとすると、今や選択肢として歴史の深いchild_processと、新機能のworker_threadsの2つの選択肢があるわけですが、ワーカー間通信の効率という観点ではどちらが優れているのでしょうか? 気になって検証しました。
検証方法
検証方法としては下記のとおりです。
- 親ワーカーと子ワーカー間で、N回メッセージの送受信を繰り返す。
- そのN回の送受信にかかる時間を測定する。
- 送受信するデータのパターンをいくつか用意し、データの内容によってどういう違いがでるかもついでに調べる。
- 各データパターンごとに1回ずつ測定。
検証コード
検証するために書いたコードが下記です。
const {repeat, data} = require('./config.js')
const {fork} = require('child_process')
if (process.send === undefined) {
// parent process
let count = 0
fork(__filename)
.on('message', function (message) {
if (message === 'end') {
console.timeEnd('test')
this.kill()
return
} else if (message === 'start') {
console.time('test')
}
this.send(++count <= repeat)
})
} else {
process.send('start')
process.on('message', continues => {
process.send(continues ? data : 'end')
})
}
const {repeat, data} = require('./config.js')
const {isMainThread, Worker, parentPort} = require('worker_threads')
if (isMainThread) {
let count = 0
new Worker(__filename)
.on('message', function (message) {
if (message === 'end') {
console.timeEnd('test')
this.terminate()
return
} else if (message === 'start') {
console.time('test')
}
this.postMessage(++count <= repeat)
})
} else {
parentPort.postMessage('start')
parentPort.on('message', continues => {
parentPort.postMessage(continues ? data : 'end')
})
}
パラメータは共通して設定できるように別ファイルにしました:
module.exports = {
repeat: 1000,
data: true,
// data: 'a',
// data: Array(10000).fill('x')
// data: 'a'.repeat(100000),
// data: Array(10000).fill({a: 1}),
}
検証結果
測定結果としては、下記のグラフのようになりました。
3つ目の1万要素ある配列を送受信するのを除くと、worker_threadsのほうがchild_processより2〜11倍速いということがわかりました。