銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

WebWorkerを手軽に扱うTransWorker v1.1でSharedWorkerが利用可能になりました!

f:id:takamints:20160131175631p:plain
photo credit: Pallet via photopin (license)

JavaScript において、メソッド呼び出し感覚で手軽に WebWorker によるマルチスレッドを扱える npmモジュール TransWorker を v1.1.0 に更新しました。

従来 DedicatedWorker にしか対応していなかったため、ワーカースレッドを生成したメインスレッドからしか扱えませんでしたが、 複数スレッドで共有できる SharedWorker が使えるようなり、ブラウザの異なるタブやウィンドウ間でワーカースレッドを共有できます。

その他、ちょっとした機能増強や不具合修正なども合わせて対応しています。

このページには、今回の変更点の概要、WebWorkerの基本事項、TransWorkerの使い方などを書いています。その他詳細はnpmのREADMEを参照してみてくださいね。

www.npmjs.com


目次

関連記事:

takamints.hatenablog.jp


TransWorker 更新内容 v1.0 => v1.1

SharedWorkerに対応【機能追加】

既に上で書いていますが、これまで DedicatedWorker にしか対応していなかったため、生成元のスレッドからしかワーカースレッドを扱えませんでした。

v1.1からは、複数スレッド間で共有できる SharedWorker が使えるようなったので、ブラウザの異なるタブやウィンドウ間でワーカースレッドを共有できます。

ワーカーからの通知のハンドラを後付け可能に【機能追加】

メインスレッド側でTransWorkerのインスタンスを生成するときに、ワーカースレッドからの通知を受け取るハンドラーをまとめて指定する形式をとっていましたが、インスタンス生成後に、個々の通知に対するハンドラーを設定できるように機能を追加しました(TransWorker.subscribe)。

メソッド呼び出し時の空のコールバックを不要に【不具合修正】

従来、メソッドの戻り値が無くてコールバックが不要な場合にでも、空のコールバック(()=>{} 等)を指定しなければいけなかったのですが、あまりにも不便でバグの元になっていたので修正しました。

引数リストの最後に関数オブジェクトが指定されていない場合は、コールバックは不要と判定します。 このチェックのため、動作速度に若干の影響があるかもしれません。 ほぼ体感できないレベルだとは思いますが、何らかの対策を考えたいです。

WebWorker について

※ここ、知ってる人は読み飛ばしてもらって結構ですよ。

WebWorkerはブラウザ上で動作するJavaScriptをマルチスレッド化するための仕組みですね。 メインスレッド(UIスレッド)からワーカースレッド(独立した実行コンテキスト)を生成し、スレッド間メッセージで通信できます。

DedicatedWorker と SharedWorker

ワーカースレッドには DedicatedWorker と SharedWorker という2つのワーカーが存在します(ServiceWorkerは別扱い)。

2つの違いは、その名前があらわしており "Dedicated" =「占有される」、"Shared" =「共有される」ということです。

つまり、DedicatedWorker は生成元のスレッドとしか通信できないのですが、SharedWorker は複数のスレッド間で共有され、各スレッドと通信可能なワーカーです。

例えばワーカースレッドで動作しているアプリケーションのコアロジックをブラウザの複数のタブやウィンドウから利用できるようになるわけです。

SharedWorkerの対応状況と制限事項

Chromeだけ?

せっかく SharedWorker に対応したのに、ざっと確認したところ、2018年12月現在 SharedWorkerがまともに動いているのは Chrome だけっぽいのです。 FirefoxではDedicatdWorkerと同様の動きをしており、EdgeやIE11では「SharedWorkerが見つかりません」と、にべもない。 SafariOpera は未確認です。

ChromeのSharedWorkerではコンソール出力が使えない?

唯一SharedWorkerに対応しているように見える Chrome で、SharedWorkerの実行コンテキスト内から console.log が使えませんでした。 エラーにはなりませんが、何も出力されません(DedicatedWorkerでは出力されます。ただし大量に出力するとフリーズしやすかった)。

このため、後述の TransWorker で、SharedWorkerを使用している場合は、メインスレッド側へ転送して出力するようにしています。

TransWorkerの概要とサンプルコード

TransWorker は、WebWorker を手軽に利用するための npm モジュールです。

ワーカースレッドで実行したい処理をクラスのインスタンスメソッドとして定義 しておけば、メソッド呼び出しと同様の手順でマルチスレッドを利用できます。 スレッド間メッセージを意識する必要はなく、普通にJavaScriptのクラスを実装する感覚で実装できます。

メソッド呼び出しをスレッド間メッセージに変換

TransWorkerは与えられたクラスのインターフェースを読み取って、メソッド呼び出しをスレッド間のメッセージ通信に変換 します。

全てのメソッドは非同期メソッドとなり、各メソッドの戻り値はコールバック関数で受け取れます。

以下に最もシンプルなサンプルを示します。もう少し複雑なサンプルはコチラにあります。

foo-bar.js

以下のクラス はひとつの値を保持するクラスで、値を設定するメソッドと、読み取るメソッドが定義されています。 これら2つのメソッドはワーカースレッドで実行されます(実際にはコンストラクタもですね)。 特別にマルチスレッドで処理したいことではありませんがサンプルなのでご容赦を。

function FooBar() {
    this._value = null;
}
FooBar.prototype.setValue = function(value) {
    this._value = value;
};
FooBar.prototype.getValue = function() {
    return this._value;
};

index.html

WEBページでは、TransWorker、FooBarクラスと、次に説明するメインスレッド側のスクリプトを読み込んでいます。 バンドラーでまとめる場合は別な形式になると思いますが、とりあえずサンプルはシンプルに書いています。

<html>
    <script src="path/to/the/transworker.js"></script>
    <script src="foo-bar.js"></script>
    <script src="main-thread.js"></script>
</html>

main-thread.js

メインスレッド側のスクリプトです。

FooBarのコンストラクタを与えてTransWorkerを生成し、それを通じてFooBarのメソッドを呼び出します。 この呼び出しはTransWorkerによってスレッド間通信に変換され、ワーカースレッド側のFooBarのインスタンスメソッドを呼び出します。 この戻り値は、コールバックで受け取ります。

//メインスレッド側ではワーカースクリプトとクラスの
//プロトタイプを参照して生成する
const fooBarWorker = TransWorker.createInvoker(
    "./worker-thread.js", FooBar);

fooBarWorker.setValue("Baz"); //戻り値が無いのでコールバック不要

//FooBar.getValueの戻り値はコールバックで受け取る
fooBarWorker.getValue(value => {
    console.log(value); // > Baz
});

woker-thread.js

ワーカースレッド側のスクリプトです。FooBarのインスタンスを指定してTransWorkerのワーカー側インスタンスを生成しています。

importScripts(
    "path/to/the/transworker",
    "./foo-bar.js");

//ワーカースレッド側ではFooBarのインスタンスを利用
TransWorker.createWorker(new FooBar());

ワーカースレッド側からの通知

また、ワーカースレッド側から(メソッド呼び出しの戻り値ではなく)能動的な通知も行えます(TransWorker.postNotify)。 SharedWorkerを使っている場合、この通知は共有するすべてのスレッドへブロードキャストされます。 ただし、ワーカーのどの通知を受け取るかは生成元または共有元の各スレッドで決定できます(TransWorker.subscribe)。

以下の例は上のサンプルを一部書き換えたものです。

foo-bar.js

function FooBar() {
    this._value = null;
}

FooBar.prototype.setValue = function(value) {
    this._value = value;

    //通知を発行する。
    this._transworker.postNotify("valueUpdated", value);
        //_transworkerフィールドはTransWorkerのファクトリが
        //注入する、このインスタンスを保持するTransWorkerの
        //インスタンスです。
};

FooBar.prototype.getValue = function() {
    return this._value;
};

main-thread.js

const fooBarWorker = TransWorker.createInvoker(
    "./worker-thread.js", FooBar);

// "valueUpdated"通知を受け取る
fooBarWorker.subscribe("valueUpdated", value => {
    console.log(`valueUpdated ${value}`);
});

//valueUpdated通知が発行される
fooBarWorker.setValue("Baz");

API

メインスレッド向け

ワーカースレッド向け

リンク

ここに記述していること以外の詳細、モジュール自体の使い方や、サンプルプログラムなどについては、以下 npm や GitHubリポジトリのREADME、またGitHub.IoのAPIドキュメントに記述してあります。