マルチスレッド: worker_threads

NodeJS 公式提供のworker_threadsモジュールを使うとマルチスレッドで実行されるコードを書けます。

スレッドの作成

Workerクラスから作ります。このコンストラクタには第一引数に別スレッドで実行するjsファイルへのパス、第二引数(オプショナル)でその別スレッドに渡す値が渡せます。

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

new Worker('/path/to/ping-pong.js', {
  workerData: {
    value: 'ping'
  }
});

送られてきたデータはworker_threadsworkerDataで受け取れます。このオブジェクトはコンストラクタで渡したworkerDataと同じ構造を持ちます。 例えば/path/to/ping-pong.jsでは以下のworkerData箇所のようにその値を使います。

const {isMainThread, workerData, parentPort} = require('worker_threads');

if (isMainThread) {
  throw new Error("メインスレッドでは実行できません");
}

const {value} = workerData;

if (value === 'ping') {
  parentPort.postMessage({value: 'pong'});
}

/path/to/ping-pong.jsはメインスレッドでの実行を想定してません。isMainThreadboolean値で「メインスレッドでの実行か?」が判断できます。
その気になればこの判断を使って、一つの.jsファイルにメインスレッドの処理と別スレッドの処理の両方を書けます。

あるスレッドからメインスレッドへデータを送り返すにはparentPost.postMessage(data)を使います。これによりメインスレッドのWorker#onmessageイベントのハンドラーでdataを同じ構造で受け取れます。

Worker#onでは他にerrorexitイベントも扱えます。これらはそれぞれ、あるスレッドの中でエラーが投げられた時や、コードが終了させられた場合に呼ばれます。


ここでは大げさにスレッドを分割してみて、

  1. 別スレッドで MarkDown ファイルREADME.mdを取得
  2. 別スレッドで MarkDown から HTML に変換
  3. 結果を表示

のようなプログラムを書いて使い方を見てみます。

README.md を取得

const {
  Worker,
  isMainThread,
  parentPort
} = require("worker_threads");
const path = require("path");
const fs = require("fs");
const { promisify } = require("util");
const readFile = promisify(fs.readFile);

if (isMainThread) {
  /**
   * @returns {Promise<string>} README.md の中身を持つ Promise
   */
  const readReadme = () => {
    return new Promise((resolve, reject) => {
      new Worker(__filename)
        .on("message", ({ contents }) => resolve(contents))
        .on("error", error => reject(error));
    });
  };

  (async () => {
    // ...
    // 略
    // ...
  })();
} else {
  (async () => {
    const contents = await readFile(
      path.resolve(__dirname, "README.md"),
      "utf-8"
    );

    parentPort.postMessage({ contents });
  })();
}

1つのファイルで2つのスレッドの処理内容を書いてます。readReadmeによって別スレッドが建てられ、そこでREADME.mdを読み込み、内容を返してます。
__filenameのように Worker には実行中のファイルと同じ名前のファイル名を渡します。このスクリプトは二度実行されることになりますが、isMainThreadにより処理を切り分けてます。

別スレッドで MarkDown から HTML に変換

今度は別ファイル<project-root>/workers/compile-markdown-to-html.jsで書いてみます。この中身は以下の通りです。ごちゃごちゃしてますが「contentsという MarkDown で書かれたテキストを受け取り、HTML に変換してメインスレッドに返す」ということをやってます。

const {
  isMainThread,
  parentPort,
  workerData
} = require("worker_threads");

if (isMainThread) {
  throw new Error("メインスレッドでは実行できません");
}

(async () => {
  const unified = require("unified");
  const markdown = require("remark-parse");
  const remark2rehype = require("remark-rehype");
  const minify = require("rehype-preset-minify");
  const stringify = require("rehype-stringify");

  const { contents } = workerData;

  unified()
    .use(markdown)
    .use(remark2rehype)
    .use(minify)
    .use(stringify)
    .process(contents, (err, file) => {
      if (err !== null) {
        throw err;
      }

      parentPort.postMessage({ file });
    });
})();

メインスレッド側に以下のコードを追記します。compileMarkDownToHTMLは、compile-markdown-to-html.jsから別スレッドで MarkDown なテキストと共に建て、変換が終わったら結果を返す非同期関数です。

/**
 * @returns {Promise<string>} HTML を持つ Promise
 */
const compileMarkDownToHTML = contents => {
  return new Promise((resolve, reject) => {
    new Worker(
      path.resolve(
        __dirname,
        "workers/compile-markdown-to-html.js"
      ),
      {
        workerData: {
          contents
        }
      }
    )
      .on("message", ({ file }) => resolve(file.contents))
      .on("error", error => reject(error))
  });
};

結果を表示

では今まで作ったreadReadmecompileMarkDownToHTMLを使って変換後結果を表示します。

(async () => {
  const readmeContents = await readReadme();
  const html = await compileMarkDownToHTML(readmeContents);

  console.log(html);
})();

リポジトリ

上記のコードはリポジトリからgit cloneyarn && node main.jsで実際に走らせれます。