Node.js v14.5.0に入ったBroadcastChannelについて

こちらは Node.js Advent Calendar 2020 の24日目の記事です。前日は @euxn23 さんによるTypeORM と比べながら Prisma を触るです!そして明日は @yosuke_furukawa さんによる締めくくりです🎅

導入

こんにちは

趣味でNode.jsのCore Collaboratorをしている@watildeです。今年は後半に時間を作ってNode.jsに21コミット行い、合計78コミットで終わりそうです。最近は本業でOSSのWeb/Mobileアプリ開発フレームワーク Amplify (公式サイト) の改善に集中をしていますが(JSerな人たちぜひ使ってみてください!)、来年は100コミット超えを目標に引き続きコミッター活動を続けていきたいと思っています。

さて、今回はNode.jsのv14.5.0で入ったworkerの新機能 “BroadcastChannel” の紹介をします。分散システムでのNode.js活用が増えてきましたが、本機能はNode.jsでWorkerを用いてマルチスレッド処理を実装する際に簡単にone-to-manyでのメッセージングを可能にするexperimentalな機能となります。APIステータスがstage 1 – experimentalなことから機能の安定はまだしていないので、プロダクトションでの利用は時期尚早かと思われます。

クリスマスプレゼント🎁

文末に、本機能の作者であるJames M Snell(@jasnell)に行ったインタビューがあります。もしよければ最後までお読みください!

BroadcastChannelの紹介

Why-What-Howの形式でなぜ作られたか・何が作られたか・何に使えるのか、の順に説明していきます。

導入された背景

BroadcastChannelは、Node.jsのTSCメンバーJames M Snellによって実装された実験的な機能です。多くのブラウザでも使えるAPIであり、WHATWGでHTML Standardの一部として定義されています。Node.jsとWebの互換性は日に日に上がっていますが、これはorganization内で設定されている優先順位に依るものだと考えています。この優先順位を明文化するのに、Node.jsコミュニティの次の10年の戦略を定めるnext-10ではWebとの互換性向上は5番目の優先項目とする方向性で議論がなされています。DOM APIが絡むケースなど、完璧なAPIの互換性の担保はできないながらも基本的に高い優先順位でマージされていく印象です。

JsamesによるPR
nodejs/node – workers: experimental BroadcastChannel #36271

一方で、後のインタビューでも出てきますが作者のJames曰く、worker間でのone-to-manyのメッセージングを簡単に行うのに必要だった、というのが動機とのことでした。コア開発者としての関心は問題解決にあったようです。

使い方

BroadcastChannelのクラスは、worker_threadsモジュールからexportされています。コード例と共に使い方を把握してみましょう。

'use strict';

const {
  isMainThread,
  BroadcastChannel, // 1. BroadcastChannelクラスのrequire
  Worker
} = require('worker_threads');

const bc = new BroadcastChannel('hello'); // 2. 各スレッドで、通信を行うために同名のチャンネルを作成

if (isMainThread) { // 3. 親スレッドの処理
  let c = 0;
  bc.onmessage = (event) => { // 6. 子スレッドから受けたメッセージを親スレッドで受信
    console.log(event.data);
    if (++c === 10) bc.close();
  };
  for (let n = 0; n < 10; n++)
    new Worker(__filename); // 4. 子スレッドの作成
} else {
  bc.postMessage('hello from every worker'); // 5.子スレッドでメッセージの送信
  bc.close();
}

実行結果

$ ./node test.js
hello from every worker
hello from every worker
hello from every worker
hello from every worker
hello from every worker
hello from every worker
hello from every worker
hello from every worker
hello from every worker
hello from every worker

使い所

CPUヘビーな処理に向いていることから、使い所はWorkerと同様なのかなと思っています。原則として、イベントループを極力ブロックせずに、同期ネットワーク呼び出しや無限ループをブロックする可能性のあるものは避けるべきだと言われています。workerを使うべきユースケースとしては、画像リサイズ、動画圧縮などがよくあげられますが、今回はファイル整合性チェックを実装してみました。コード例は watilde/broadcast-api-example にて公開しています。

const {
  Worker,
  isMainThread,
  BroadcastChannel, 
  workerData
} = require('worker_threads');
const crypto = require("crypto");
const fs = require("fs");
const files = [{
  path: 'a.png',
  hash: 'c7c3c7f24d1655ee6f7936044afa8f7ed8ad9d64'
}, {
  path: 'b.png',
  hash: '4920d622b60e1025a12e237ee9e71362df408c3b'
}];
// BroadcastChannelの生成
const bc = new BroadcastChannel('check'g);

if (isMainThread) {
  let count = 0;
  files.forEach((file) => {
    // Workerの生成
    new Worker(__filename, {workerData: file.path});
    // 子スレッドからのメッセージ受信
    bc.onmessage = (event) => {
      const paths = files.map(item => item.path);
      const index = paths.findIndex(path => path === event.data.path);
      // 整合性チェック
      console.log(
        `Hash check - ${event.data.path}`,
        files[index].hash === event.data.hash
      );
      if (++count === files.length) bc.close();
    };
  })
} else {
    const algorithm = "sha1";
    const shasum = crypto.createHash(algorithm);
    const stream = fs.ReadStream(workerData);
    stream.on("data", function(data) {
      shasum.update(data);
    });
    stream.on("end", function() {
      const hash = shasum.digest("hex");
      // 親スレッドにメッセージ送信
      bc.postMessage({path: workerData, hash: hash});
      bc.close();
    });
}

その他、workerと他の分散処理モジュールの比較は@suinさんのNode.js: CPU負荷で3秒かかっていた処理を「Worker Threads」で1秒に時短するや、@about_hiroppyさんのNode.jsにworkerが入った – 技術探しを参照ください。この記事では、@suinさん作の比較図を紹介します(掲載許可いただきありがとうございます!)。

@suinさん作の比較図
Node.js: CPU負荷で3秒かかっていた処理を「Worker Threads」で1秒に時短する

作者のJamesへのインタビュー🎁

作者のJames M Snellと雑談している中で、記事を書いている旨を伝えたら質問に答えてくれる流れになったので開発の時系列を軸に、5つの質問をしてみました。

Q1. [実装を行う前の話] Broadcast Channel APIを実装するに至った理由は何かありますか?

A1. Poolまたは複数のWorkerを操作する場合、すべてのWorker間で通信するためのMessagePortの管理が困難になり、パフォーマンスの低下が起きました。 BroadcastChannelを開発した動機は、管理コストなしで、高速、効率的、かつ単純なone-to-manyの通信を可能にすることでした。 ブラウザとの互換性向上は結果的に発生したものです。

Q. [Before you start] What was the motivation when you implement Broadcast channel API?

A. When working with pools or collections of workers, managing MessagePorts for communication across all workers becomes difficult and does not perform well. The sole motivation for BroadcastChannel was to enable fast, efficient, and simple one-to-many messaging without the management overhead. Browser compatibility was just a bonus

Q2. [実装中の話] Broadcast Channel APIを実装をする際に気をつけたことは何ですか?

A2. パフォーマンスを最も気にしました。ブラウザのAPIとの互換性も同じレベルで気をつけたと思います。

Q. [While you do] What did you care when you implement Broadcast channel API?

A. Performance was the first concern. Compatibility with the browser api was a close second.

Q3. [実装後の話] 最も印象に残っているレビューコメントは何かありますか?

A3. 私の実装に対して、Anna Henningsen(@addaleax)がいつものように素晴らしいフィードバックをしてくれました。彼女のレビューにより、PRのdiffを最小化しながらパフォーマンスがさらに向上されました。

Q. [While you get reviews] What feedback do you remember the most?

A. As usual, addaleax (Anna H) provided fantastic feedback on the implementation, allowing it to perform even better with minimal changes to existing code.

Q4. [マージ後の話] PRがマージされてから、実際にNode.jsの開発者から何かフィードバックを受けましたか?

A4. ポジティブなコメントばかり受け取っています。Annaは、Node.jsのコアにてWeb LocksAPIをBroadcastChannelを使用しながら実装することを検討しています。 利用者にとってはまだ新しい存在だと思います。

Q. [After you finish] Did you receive any comments from Node.js developers after your PR got merge?

A. All positive. Anna is currently exploring using BroadcastChannel within a Web Locks API implementation for core. I think for others it’s still very new.

Q5. [今後の話] Broadcast Channel APIに興味のある開発者に一言お願いします!

A5. 全てのメッセージはcloneされることに気をつけてください!cloneのコストを爆発させないために、メッセージはシンプルなものにしましょう。

Q. [In future] Could you give us a comment to developers who are interested in Broadcast channel API?

A. Remember that all messages are cloned! Keep messages relatively simple to avoid additional costs.

Jamesさんありがとうございました!

Thank you James for kindly responding to my interview questions!

参考リンク