こんにちわ。
今回は、みんな大好きWebWorkerのお話です。
新しいインターフェイスとその使いかたを知ったので、まとめてみようと思います。
普通のWebWorker
WebWorkerを使うとき、普通はWorker
に備え付けられたメッセージングインターフェイスでスレッド間の通信を行うと思います。
const worker = new Worker("./worker.js");
worker.addEventListner("message", ({data})=>{});
worker.postMessage("ほげほげ", []);
// self => DedicatedWorkerGlobalScope
self.addEventListener("message", ({data})=>{
self.postMessage("ほげほげ");
});
普通に使うなら、これで充分な場合がほとんどです。
特にDedicatedWorker
の場合、そのコンテキスト内で処理を完結させるケースがほとんどなので、困ることはないと思います。
しかし、例えばバックエンドサーバーとクライアントアプリケーションの間でAPIの仲介を行う、半永続的なワーカースレッドを生やすとします。
この場合、アプリケーションのあらゆるコンテキストからワーカースレッドへアクセスされることが予想されます。
ワーカースレッドを複数のコンテキストで共有したい場合、まずSharedWorker
が浮かぶと思います。
しかしこのSharedWorker
は、コンテキストを跨いだメッセージングを行うためにDedicatedWorker
では隠蔽されていたMessagePort
というメッセージングインターフェイスを操作する必要があり、これが少々厄介です。
const worker = new SharedWorker("./worker.js");
worker.port.start();
worker.port.addEventListener("message", ({data})=>{});
worker.port.postMessage("ほげほげ", []);
// self => SharedWorkerGlobalScope
self.addEventListener("connect", ({ports})=>{
ports[0].addEventListener("message", ({data})=>{
ports[0].postMessage("ほげほげ", []);
});
ports[0].start();
});
* 厳密にはDedicatedWorker
のメッセージングインターフェイスにもMessagePort
は存在しますが、コンテキストはひとつなのでユーザーからは隠蔽され暗示的に操作されます。
BroadcastChannel
BroadcastChannel
は、コンテキストを跨いだメッセージングを簡単に行うためのインターフェイスです。
コンストラクタでチャネルを指定でき、そのチャネルが存在しない場合は作成し、存在する場合は参加します。
作成もしくは参加した後は、ブロードキャストと名にある通りそのチャネルへ参加しているコンテキストへメッセージを配信できます。
メインスレッド内同士はもちろんワーカースレッドやiframeなども横断してアクセスできます。
BroadcastChannel
も他のメッセージングインターフェイスと同様にChannel Messaging APIがベースとなっているため、実際のコーディングも似たような感じとなっています。
BroadcastChannel.prototype.postMessage()
で、そのチャネルへ接続しているすべてのコンテキストへ向けてメッセージを配信します。
BroadcastChannel.prototype.onmessage
で、配信されたすべてのメッセージを購読します。
BroadcastChannel.prototype.close()
で、チャネルへの接続を終了し、コンテキストをガベージコレクタに回収してもらえるようにします。
// "my-channel"は存在しないため作成される
const ctx1 = new BroadcastChannel("my-channel");
ctx1.addEventListner("message", ({data})=>{
ctx1.close();
});
ctx1.postMessage("ほげほげ");
// "my-channel"は存在するため参加される
const ctx2 = new BroadcastChannel("my-channel");
ctx2.addEventListner("message", ({data})=>{
ctx2.close();
});
ctx2.postMessage("ほげほげ");
Worker × BroadcastChannel
WebWorkerでスレッド間通信を行う場合、備え付けのメッセージングインターフェイスを使用するのではなく、代わりにBroadcastChannel
を使うことができます。
new Worker("./worker.js");
const ctx1 = new BroadcastChannel("worker-channel");
ctx1.addEventListener("message", ({data})=>{
ctx1.close();
});
ctx1.postMessage("ほげほげ");
const ctx2 = new BroadcastChannel("worker-channel");
ctx2.addEventListener("message", ({data})=>{
ctx2.close();
});
ctx2.postMessage("ほげほげ");
const bc = new BroadcastChannel("worker-channel");
bc.addEventListener("message", ({data})=>{
bc.postMessage("ほげほげ");
});
本来であればDedicatedWorker
は複数のコンテキストを跨ぐことはできませんがBroadcastChannel
と組み合わせることでMessagePort
を隠蔽しつつコンテキストを跨ぐことができます。
なおWorker
コンストラクタは、宣言した時点でスクリプト解析とスレッド生成と実行が開始されるため、ワーカースレッドを強制停止(Worker.prototype.terminate()
)させる必要がなければ、インスタンス変数を充てがう必要はありません。
メリット
まずSharedWorker
と違いMessagePort
を操作する必要がないため、複数のコンテキストが存在していたとしてもメッセージングを簡単に実装できます。
また、ワーカースレッドとメッセージングのオブジェクトを分離することにより、コードの見通しが良くなるという利点も存在します。
ひとつのワーカースレッドに複数のチャネルを設けて集中処理こさせることも可能ですし、メッセージを受け取って処理するスレッドをワーカースレッドからメインスレッドへ移す場合もBroadcastChannel
のブロックを丸ごと移動させるだけで済みます。
特に最近のコンポーネント志向なフロントエンドフレームワークにおいては、強力な武器となるでしょう。
デメリット
BroadcastChannel
は比較的ローレベルでシンプルなメッセージングインターフェイスのため、やりとりするメッセージに識別子を埋め込むなどして、目的のメッセージのみを購読するためのロジックを用意する必要があります。
また、最大のデメリットはBroadcastChannel
にはTransferable
インターフェイスが存在しません。
やりとりされるメッセージは全てコピーされます。
つまり、巨大なバイナリバッファのやりとりは不向きとなります。