child_process#fork()とWorkerの比較の画像
芽萌丸プログラミング部 @programming
投稿日 2022/02/10

Node.js

child_process#fork()とWorkerの比較

Node.jsでスレッド処理っぽいことをする上で、child_processモジュールのfork()worker_threadsモジュールのWorkerで使いどころが割と被るのですが、実際どちらが性能が良いのかを簡単に調査してみました。

目次:

環境

  • Ubuntu 16.04.7 LTS
  • Node.js v16.13.2

結論

先に結論からいうと、筆者のLinux環境では以下の点でWorkerに軍配が上がりました。(もちろんマシン環境等にもよると思います。)

  • 両者とも処理速度は殆ど変わらない
  • Worker はイベントループのブロッキングが明らかに小さい:
    • Worker は呼び出し元に 3 ms 以上のブロッキングは無し。
    • 一方、forkは呼び出し元に 3 ms 以上のブロッキングが発生。

以下は実証コードと実行結果です。

実証コード

fork版

main_fork.js (メイン)

// イベントループのブロッキング調査:
require('blocked-at')((time, stack) => {
  console.log(`Main process blocking: ${time}ms block detected.`);
}, { threshold: 3 }); // 閾値は3ms

const { fork } = require('child_process');

(() => {
  const start = process.hrtime();

  // 重い処理をメインスレッドから切り離す
  const child = fork(__dirname + '/getCount_fork.js');

  child.on('message', (message) => {
    console.log('results from forked process:', message);
    const diff = process.hrtime(start);
    console.log(diff[0] * 1e9 + diff[1]);
  });

  child.send('START');
})();

getCount_fork.js (呼び出し先)

// fork版: getCountの処理結果をメインへ返す

process.on('message', (message) => {
  if (message == 'START') {
    console.log('Child process received START message');
    const slowResult = require('./getCount.js')();
    const message = `totalCount: ${slowResult}`;
    process.send(message);
  }
});

Worker版

main_worker.js (メイン)

// イベントループのブロッキング調査:
require('blocked-at')((time, stack) => {
  console.log(`Main process blocking: ${time}ms block detected.`);
}, { threshold: 3 }); // 閾値は3ms

const { Worker } = require('worker_threads');

(() => {
  const start = process.hrtime();

  // 重い処理をメインスレッドから切り離す:
  const workerFile = __dirname + '/getCount_worker.js';
  const worker = new Worker(workerFile);

  worker.on('message', (message) => {
    console.log('results from worker process:', message);
    const diff = process.hrtime(start);
    console.log(diff[0] * 1e9 + diff[1]);
  });

  worker.postMessage('START');
})();

getCount_worker.js (呼び出し先)

// worker版: getCountの処理結果をメインへ返す

const { parentPort } = require('worker_threads');

parentPort.on('message', (message) => {
  if (message == 'START') {
    console.log('Child process received START message');
    const slowResult = require('./getCount.js')();
    const message = `totalCount: ${slowResult}`;
    parentPort.postMessage(message);
  }
});

共通処理

getCount.js (とても遅い処理)

// とても遅い処理
module.exports = function getCount() {
  let counter = 0;
  while (counter < 5000000000) {
    counter++;
  }
  return counter;
}

実行結果

fork版:

$ node main_fork.js 
Child process received START message
results from forked process: totalCount: 5000000000
5651059080
Main process blocking: 3.386962999343872ms block detected.

3 ms 以上のブロッキングが発生!

次にWorker版:

$ node main_worker.js 
Child process received START message
results from worker process: totalCount: 5000000000
5875327627

僅かに処理は遅い(と言っても誤差の範囲内)が、ブロッキングの発生は無し。

以上です。


芽萌丸プログラミング部
芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。記事は主に @TanakaSoftwareLab が担当。