銀の弾丸

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

JavaScriptのWebWorkerでスレッド間のリモートプロシジャコール(RPC)を実装する

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


ブラウザのJavaScriptでWebアプリをマルチスレッド化できる Web Workers API の基本と、Worker側を使ってスレッド間でのリモートプロシジャコールを実装して、ワーカースレッドのメソッドを呼び出して戻り値をコールバック関数で受け取れるようにする方法を書いています。

■■■ Table Of Contents ■■■

  1. Workerの基本1:ワーカースレッドとの通信
  2. Workerの基本2:スレッド間メッセージはコピーされる
  3. しかしメソッド(関数オブジェクト)はコピーされない(送信できない)
  4. 同じくクラスオブジェクトのメソッドも無理ですよ
  5. コピーではなく参照を渡す方法もある(らしい)
  6. スレッド間リモートプロシジャコール(RPC)の実装
  7. npm transworkerのご紹介
  8. おまけ1:スクリプト内でコンテキストを判断するには?
  9. おまけ2:関数定義は常にグローバルスコープなんだな

  10. その他ワーカーについては、MDNの「Web Worker を使用する」を参考にしてください。

  11. 以降で使用しているWorkerはDedicatedWorker (専用ワーカー)というもので、サブスレッドを生成したスレッドと一対一の通信を行えるものです。他にSharedWorkerというのもあります。



Workerの基本1:ワーカースレッドとの通信

メインスレッドとワーカースレッドの間での通信は、メッセージを使用します。

メインスレッド側スクリプト:

var wkr = new Worker("dedicated/worker/thread.js");

//ワーカーへメッセージ送信
var data = {key1:value1, ... };
wkr.postMessage(data);

//ワーカーからのメッセージを受信
wkr.onmessage = function(e) {
    console.log("main thread onmessage e:" + JSON.stringify(e));
}

ワーカー側スクリプト:

//メインスレッドのメッセージを受信する
onmessage =  function(e) {
    console.log("worker thread onmessage e:" + JSON.stringify(e));
}

//メインスレッドへメッセージを送信する。
var data = {key1:value1, ... };
postMessage(data);

簡単ですね。

メインスレッドでは、生成したワーカーのpostMessageとonmessageで、送受信が行えます。

ワーカー側では、グローバルコンテキストでpostMessageとonmessageを使用できます。

Workerの基本2:スレッド間メッセージはコピーされる

postMessage ~ onmessageでやり取りされるメッセージは、コピーされます。 スレッド間で同じオブジェクトを共有していないため、それぞれ安心して自由に使用できます。 (ただし、特別な方法でオブジェクトの参照を渡すことも可能(↓)です。)

しかしメソッド(関数オブジェクト)はコピーされない(送信できない)

少なくともChromeでは関数オブジェクトを送信できませんでした。 他のブラウザでは試していませんが、多分出来ないんじゃないかな。 関数オブジェクトを一旦文字列化してからevalすればよいと思われますが、パフォーマンス上は望ましくないと思います。

むしろ、関数オブジェクトを送信しなくてもよいように設計するべきだと思います。

同じくクラスオブジェクトのメソッドも無理ですよ

関数オブジェクトが送信できないので、当然のようにクラスオブジェクトのメソッドも送信できません。

しかし、データメンバーは送信されます。

送信できないオブジェクトをシリアライズするときに、あえて排除しているのだとすると、 パフォーマンス的にデメリットがあるかもしれません。 特に詳しく確認していませんが、そのうち調べてみようとは思っています。

コピーではなく参照を渡す方法もある(らしい)

Transferableインターフェースを実装するデータは、 コピーではなくオブジェクトそのものを送信できるようです。(⇒所有権の譲渡 (Transferable Objects) によるデータの引き渡し|MDN)) ただし、送信元では、それ以降そのオブジェクトを使えなくなります。文字通り渡してしまうようなイメージになります。

// 32MB の "file" を作成して埋めます。
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i;
}
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

上のコードは、MDNのサンプルをそのまま引用しています。postMessageの引数が違っていますね。 第2引数は配列で、その要素は第一引数ということのようですが、これに実質的にどんな意味があるのかはよくわかりません。あまり直感的でないよう・・・。

  • Transferableインターフェースを実装しているクラスは、ArrayBufferMessagePort だけとのことです。

スレッド間リモートプロシジャコール(RPC)の実装

これまで書いたように、スレッド間メッセージは、それぞれが好きなときに送信でき、非同期で処理されますが、関数オブジェクトは転送できません。 そして、スレッド間のリモートプロシジャコールの機構も提供されていないようなので自前で用意しなくてはならないようです。

以下のフローは、サブスレッドで定義されるメソッドを呼び出し、戻り値をコールバックで受け取る処理を示しています。

(要求元)
1. 送信する要求メッセージとコールバック関数のセットに対して、新たなIDを割り当てます。
2. 送信元で、このコールバック関数を、IDをキーとしたハッシュに保存
3. 送信先へ要求メッセージとIDを送信

(ワーカー側)
4. メッセージを受信し、要求に対する応答メッセージを生成
5. 受信メッセージ内のIDと応答を返却

(再び要求元)
6. 応答を受信し、そこに含まれるIDでコールバックを検索。
7. 検索したコールバックのパラメータに応答データを指定して呼び出します。

npm transworkerのご紹介

上記フローを実装したモジュール transworker をnpmで公開しています。 概要は下図参照。詳細はREADMEに書いていますので、よろしくどうぞ。

TransWorkerの概念図

TransWorkerクラスは、下図(?)のように、シングルスレッドでのクラスメソッド呼び出しモデルの間に入って、そのクラスの処理を丸ごとサブスレッド側へ持っていくことです。 メソッドの戻り値が、コールバックで得られるということを覚えておけば、利用者側は細かいことを気にする必要はありません。


                      Single thread model
                      ===================

    +-----------+        call methods         +-----------+
    |Application|---------------------------->|ClientClass|
    +-----------+                             +-----------+
          ^                                         |
          |                 return                  |
          +-----------------------------------------+



             Multi thread model(using TransWorker)
             =====================================

                               |
           Main (UI) thread    |    Web Worker (sub) thread
                               |
              call methods     |     call methods
                   |           |          |
                   |   +---------------+  |
                   |   |    message    |  |
                   |   |    ------>    |  |
    +-----------+  V   |               |  V   +-----------+
    |Application|----->|  TransWorker  |----->|ClientClass|
    +-----------+      |               |      +-----------+
          ^            |    message    |            |
          |            |    <------    |            |
          |            +---------------+            |
          |               |    |    ^               |
          |    callback   |    |    |    return     |
          +---------------+    |    +---------------+
                               |

www.npmjs.com github.com

おまけ1:スクリプト内でコンテキストを判断するには?

上のクラスでやっているように、グローバルスコープの this.constructor.name でコンテキストが判断できます。 これが、'Window'なら、ブラウザのUI-スレッド(メインスレッド)で読み込まれていることを示しており、'DedicatedWorkerGlobalScope'なら、Workerだということです。

おまけ2:関数定義は常にグローバルスコープなんだな

Worker無関係ですが、グローバルコンテキストで処理を振り分け、ThenとElseブロックの両方で、同名の関数定義を行うと、後に定義された関数しか参照できません。

説明が分かりにくいって人、ごめんなさい。以下のスクリプトを見てください。

var context = this.constructor.name;
if(context == 'Window') {
  function ComWorker() {
    console.log("ComWorker for UI-thread");
  }
} else if(context == 'DedicatedWorkerGlobalScope') {
  function ComWorker() {
    console.log("ComWorker for Worker-thread");
  }
}

このスクリプトをメインスレッド側で読み込んだ場合でも、new ComWorker()を実行すると、コンソールに、"ComWorker for Worker-thread"が表示されていました。

関数定義の評価はスクリプト読み込み時に処理されて、グローバルコンテキストに定義されるんでしょうね。勘違いしやすいので気をつける。

あとがき

ブラウザで動作する往年の8ビットマイコン MZ-700エミュレータを作っており、 当初、インターバルタイマーを使って単一スレッドで実装したら速度が遅くて不安定。ブラウザが操作不能に陥ったりしていました。

そこで、小耳に挟んだWeb Workers APIを調査開始。 ワーカースレッド側でタイマーを使えば動作が安定。マシンスペックに応じて可能な限り高速に動作するようになりました。

現在エミュレータでは、Transferableインターフェースでデータを移譲する方法を使用していませんが、 サブスレッド側でクローンしてからメイン側へ移譲するなどすれば、 ある程度の速度向上に役に立ちそう。

関連サイト:

takamin.github.io takamin.github.io

C++の参照型の落とし穴:クラスメンバに参照型は使わないほうが良さそうだ

先日来、C++のデストラクタで、おかしな動きにぶち当たり「おかしい!バグか?」と大騒ぎした後、最終的に自分のミスに気がつきました。

タイトル通り、クラスにおける参照型に関する落とし穴です。

経緯とともにホントにお恥ずかしい限りですが、忘れた頃に同じことを繰り返しそうなので、恥を忍んで書いておきます。


f:id:takamints:20151227174609j:plain
photo credit: The Arm of Destruction via photopin (license)


プログラミング言語C++第4版
ビャーネ・ストラウストラップ Bjarne Stroustrup
SBクリエイティブ
売り上げランキング: 5,351

経緯はこう

  1. クラスのインスタンスメンバに参照型を使ったシンプルなクラスを定義。
  2. そのメンバをデストラクタで使っていたが正しく動作してないようだ
  3. 「デストラクタが動いてないってどういうこと?!」と大騒ぎ(←最初の勘違い)
  4. 落ち着いて、確認用のコードを書いてみたら、デストラクタは動いていた
  5. しかしデストラクタ内では、そのメンバの値が正しくないコンストラクタでは正しかったが?
  6. 「おいおいデストラクタで参照型が使えないってどういうこと?!」と大騒ぎ(←2つめの勘違い)
  7. ここまで全てVisual C++で動かしていたけど、G++では想定通りに動いていた
  8. 「ほうらやっぱり Visual Studio おかしいぞ!」と大声出したら急に自信がなくなって、、、
  9. じっくりコードを眺めてみたら、結局コンストラクタに単純ミスが。。。(自分か)

これが問題のクラス定義だ

スレッド間での排他処理のために、以下の様なクラスを定義したのです。

class MutexLocker {
    HANDLE& mutex;
public:
    MutexLocker(HANDLE mutex) : mutex(mutex) {
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

コンストラクタミューテックスをロックして、そのスコープから抜けるときにデストラクタで開放するというものですね。 (※ Visual Studio 2012以降では、スレッド間排他処理のために、CriticalSectionというクラスがあるようですので、そちらを使うほうが良いらしい。それ以前のバージョンではCRITICAL_SECTION構造体を使用。Mutexはプロセス間でも排他処理が可能なロックオブジェクトです)

ところが

以下のように使用しても、ミューテックスが開放されません。 デストラクタ内ではmutexを正しく参照できなくて、どこの馬の骨ともしれないHANDLEを開放している。 なぜだ。

class PacketLogger : Thread {
private:
    HANDLE mutex;
    std::deque<Packet*> packet_queue;
public:
    //   ・
    //   ・
    //   ・
    void AddPacket(Packet* p) {
        MutexLocker lock(mutex); //←ココ
        {
            packet_queue.push_back(p);
        }
    }
    //   ・
    //   ・
    //   ・
}

こうすればちゃんと動いていた

デストラクタで使用するメンバを参照でなく実体にすれば問題なかった

class MutexLocker {
    //HANDLE& mutex; //←参照でなく
    HANDLE mutex;    //←実体に
public:
    MutexLocker(HANDLE mutex) : mutex(mutex) {
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

確認コードを書いたはいいがむしろ勘違いを補強する

確認用に以下のコードを書いてみて、Visual Studio の3つのバージョン、2010、2013、2015で確認しても(2012は手元にない)、結果はすべて同じでした。DtorTestClassA のデストラクタでは、コンストラクタで出力した値を出力できない。 最初に書きましたが、MinGW上のG++では、想定通りに動いていましたので、「VisualStudioでは、デストラクタで参照が壊れている」と考えちゃって大騒ぎ。

#include <iostream>

class DtorTestClassA {
    const int& number;
public:
    DtorTestClassA(int number) : number(number) {
        std::cerr << "construction DtorTestClassA #" << number << std::endl;
    }
    virtual ~DtorTestClassA() {
        std::cerr << "destruction DtorTestClassA #" << number << std::endl;
    }
};
class DtorTestClassB {
    const int number;
public:
    DtorTestClassB(int number) : number(number) {
        std::cerr << "construction DtorTestClassB #" << number << std::endl;
    }
    virtual ~DtorTestClassB() {
        std::cerr << "destruction DtorTestClassB #" << number << std::endl;
    }
};
int main(void) {
   DtorTestClassA a1(1);
   DtorTestClassB b1(1);
   {
       DtorTestClassA a2(2);
       DtorTestClassA b2(2);
   }
   for(int i = 0; i < 3; i++) {
       DtorTestClassA a3(i + 3);
       DtorTestClassA b3(i + 3);
   }
}

Visual Studio のバグだろコレ!」って、んなわけないし。

ここで再び、じっくりコードを見なおしてみると、、、別のところがおかしいよと。

class MutexLocker {
    HANDLE& mutex;
public:
    MutexLocker(HANDLE mutex) : mutex(mutex) {
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

コンストラクタおかしくないかい?」と。

コンストラクタの引数リストが、参照型になってないよと。

以下のようになっていないとダメじゃないかと?

class MutexLocker {
    HANDLE& mutex;
public:
    MutexLocker(HANDLE& mutex) : mutex(mutex) { //←ココだ
        WaitForSingleObject(mutex, INFINITE);
    }
    virtual ~MutexLocker() {
        ReleaseMutex(mutex);
    }
};

元のコードでは、メンバがコンストラクタのパラメータを参照していて、それってつまりコンストラクタの処理が終われば消滅しているオブジェクトですがな。そらあかんわ。

まとめと所感・教訓および反省文

検証大切

最初のコードを見た段階で気付ける人は幸せです。私は無理でしたが。 気付けないのはまあ、アタマの程度の問題なので仕方がないけど、「ああ、こういうことなんだろうな?」という推測で進んだのがダメダメですな。お恥ずかしい。

人って基本的に、他者を疑うようになっていると思うけど、疑念を公言する前に各方面から確実に検証しなければ、こんな恥ずかしいことになるって事例です。大反省。

未来永劫クラスメンバで参照型は使わない

絶対にそうしなくちゃならないという理由なく、クラスメンバに参照型は使わないほうが良いかもしれないですね。

対象が構造体やクラスオブジェクトの場合はポインタで、完全になんの問題もなく代用可能ですから、そういう理由は今のところ見当たりません。

そもそもなんで最初に参照型にしたのかというと、明確に「そうしなくては」と思ったわけではなく「そのオブジェクトの生成と消滅には直接関わりたくない」という感覚的なものだった。

ところでG++の実装や如何に?

それから、むしろ、G++の動きのほうが怪しいという結果になったけど、もしかすると気を利かせて、こちらの意図に沿うように解釈してくれていたのかも? いや、単にスタックフレームの使い方が違うのかもしれませんね。

無視しないから警告出してよ

いやしかし、自分のミスを棚に上げてでも言っておきたいのだが、クラスメンバがコンストラクタのパラメータを参照しちゃっているのは明らかに間違いなんだから、せめてコンパイル時に警告出してほしいわ。静的解析したら警告なのかな?というか出てた?(←これがよくない)

んなわけないし→デストラクタで参照型のメンバ変数が使えない?

この記事無効です。自分の単純ミスでした。

違う、そうじゃない
違う、そうじゃない
posted with amazlet at 15.12.28
Epic Records Japan Inc. (2013-10-23)
売り上げランキング: 28,637

気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。

takamints.hatenablog.jp

f:id:takamints:20151227174609j:plain
photo credit: The Arm of Destruction via photopin (license)


プログラミング言語C++第4版
ビャーネ・ストラウストラップ Bjarne Stroustrup
SBクリエイティブ
売り上げランキング: 5,351

大間違い→デストラクタが自動的に呼ばれない

この記事無効です。自分の単純ミスでした。

違う、そうじゃない
違う、そうじゃない
posted with amazlet at 15.12.28
Epic Records Japan Inc. (2013-10-23)
売り上げランキング: 28,637

気を取り直して自分のミスの暴露記事は下記参照。何卒よろしくお願いいたします。

takamints.hatenablog.jp

f:id:takamints:20151227174609j:plain
photo credit: The Arm of Destruction via photopin (license)


テキストを日本語的に傍点(圏点)で強調するJavascript

ふと思いついてテキストに傍点をつけるためのスクリプトを作りましたので、ご紹介。(ソースはGitHubに置いてます)

「傍点」は「圏点」ともいうらしいのですが初耳でした。この文書では以降「傍点」で通します。

f:id:takamints:20151128182516j:plain

「傍点によるテキストの強調」は、CSS3のtext-emphasisスタイルで定義されていますが、ブラウザによって対応状況が大きく分かれているようなので、このスクリプトを作った次第。

SafariChrome等、Webkit系の「傍点対応ブラウザ」では素直にそのままCSSで表現しますが、IEFirefoxなどの未対応のブラウザでは「ルビ(RUBYタグ)」を利用して、なんとかしました。

ウェブ上で傍点は、あまり使用することがありませんが、強調というよりも単語の区切りをはっきり示すために使いたい事があります。特に、句読点を入れると、文章のリズムに違和感を感じるような場合などですね。

※ ルビに未対応のブラウザでは、かなりおかしな表示になってしまいますが、傍点よりは対応状況が良いようです。2015年11月の時点で、IEFirefoxの最新版は、全てRUBYに対応してます。Operaプラグインで対応できると聞きましたが、使用していないので詳しく分かりません。


ゲームで学ぶJavaScript入門 HTML5&CSSも身に付く!
田中 賢一郎
インプレス (2015-12-11)
売り上げランキング: 25,808

傍点のスタイル

CSStext-emphasis-styleで指定するのと同じ表現が可能です。

style filled open
dot 横転 好転
circle 争点 当店
double-circle 脳天 法典
sesame 盲点 0点
triangle 栄転 経典

簡単な適用

class='bauten-text-emphasis'となった要素に傍点をつけるのなら、bauten.jsを読み込むだけです。この場合は、黒丸(filled dot)付けられます。

gist.github.com

要素とスタイルを指定する

傍点を適用する要素や、そのスタイルを指定するには、bauten.jsを読み込んだ後のSCRIPTで、bauten関数を呼び出します。

引数は以下のキーを持つオブジェクトです。

キー 説明
className 傍点を打つ要素のクラスを指定します。複数のクラス名をスペースで区切って指定できます。この場合は全てのクラス名を持つ要素が対象になります。
tagName 傍点を打つ要素のタグ名を指定します。
style 傍点のスタイルを指定します。CSSのtext-emphasis-style と同様の指定方法です。
color 傍点の色を指定します。

※ classNameとtagNameのどちらかは指定されなくてはなりません。styleは必須。colorが省略されるとベースの文字色に従います。
※ bauten関数を呼び出した場合、既定の動作は抑制されます。

例:クラス名による指定

gist.github.com

例:タグ名による指定

gist.github.com

任意の文字を傍点にする

任意の文字を傍点にできます。これもtext-emphasis-styleと同様の機能です。

  • たとえばーきみがいるだーけで
  • にわにはにわにわとりがいる。

gist.github.com

傍点の色を指定する

傍点には色を付けられます。これもtext-emphasis-colorと同等。

ブルー・ノート・スケール(ブルース・スケール、blue note scale)は、ジャズやブルースなどで使われる、メジャー・スケール(長音階)に、その第3音、第5音、第7音を半音下げた音を加えて用いるもの、もしくはマイナー・ペンタトニック・スケールに♭5の音を加えたものである。特に、♭5の音をブルー・ノートと呼ぶ。近代対斜の一種でもある。

gist.github.com

リポジトリ

bauten.js は、GitHubの以下のリポジトリで公開しています(MITライセンス)

github.com