銀の弾丸

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

npm 「hash-arg」 ― コマンドラインパラメータに名前でアクセス

コマンドラインで指定されたパラメータに名前でアクセスする為の、シンプルなNode.jsのモジュール hash-argをnpmで公開したので御紹介します。

npm便利ですね。若干敷居が高い気がしていましたが、まずはいろんなモジュールを検索して使うところから始めるといいですね。

f:id:takamints:20160403163627p:plain

'hash-arg'コマンドラインパラメータに名前でアクセスするためのシンプルなnpmモジュールです。

www.npmjs.com

ソースはGitHibに置いています。

github.com


JavaScript 第6版
JavaScript 第6版
posted with amazlet at 16.04.03
David Flanagan
オライリージャパン
売り上げランキング: 16,569

はじめに

このモジュールは、コマンドラインオプションの処理をするものではありません

単純に、コマンドライン引数、または配列の各要素に、前から順に名前を付けてハッシュ(JavaScriptのobject)を返すモジュールです。 追加の機能として、各パラメータの型指定と、省略時の既定値の指定ができますが、基本的には単純なものです。

Node.jsのコマンドラインパラメータは、process.argv[2] 以降に文字列として格納されています([0][1]"node"と、スクリプト名)から、本モジュールにパラメータの定義(名前、型、既定値)だけを与えた場合は、process.argv[2]以降の要素を処理対象とします。 別の配列を与えた場合は、すべての要素を処理します。

オプションが含まれる場合

コマンドライン引数に'-'で始まるようなオプションを含ませたい場合は、先にnode-getoptのようなオプションのパーサーで処理しておいて、そのモジュールが(おそらく)提供している、「オプションではない引数の配列」を、当モジュールで処理させます。

少しだけ気楽になるモジュールです(多分)

オプションではない引数が配列に入っている場合、その要素数をチェックして、必要なパラメータが足りていなかったりすると、エラーにしたり、省略時の既定値を設定したりしますよね。これって、少し邪魔くさい。

本来ザクザク書きたいのはそういうところじゃないはずです。 どの位置に何が指定されていたのか?という情報は、その後、使わない情報ですし。

あと、最初のパラメータなのにprocess.argv[2]とか書いちゃうあの妙な違和感(笑)もどうにかできます。

使い方:単純に'process.argv'を使用する場合

唯一のメソッドgetの第一引数にパラメータ名の定義を与えて呼び出せば、process-argvから値を取り出し、オブジェクトの指定されたキーに値を格納して返します。 (第二引数は省略可能なパラメータの配列です)

simple.js

args = require("hash-arg").get(
        "inputFilePath outputFilePath");
 
console.log(JSON.stringify(args, null, "  "));

実行例:

$ node test/simple.js input.json output.json
{
  "inputFilePath": "input.json",
  "outputFilePath": "output.json"
}

使い方:node-getoptのようなコマンドラインパーサーと共に使用

省略可能な第二引数に、たとえば、node-getoptモジュールの argv プロパティを与えればOK。

with-node-argv.js

getopt = require("node-getopt").create([
    ['s', '', 'short option'],
    ['l', 'long', 'long option'],
    ['S', 'short-with-arg=ARG', 'option with argument']
]).parseSystem();
 
args = require("hash-arg").get([
        "inputFilePath",
        {
            "name":"outputFilePath",
            "default": "out.json"
        }
        ], getopt.argv);
 
console.log(JSON.stringify(args, null, "  "));

実行例:

$ node test/with-node-getopt.js -S DUMMY input.json -sl output.json
{
  "inputFilePath": "input.json",
  "outputFilePath": "output.json"
}

GET メソッド

呼び出し形式

HashArg.get(<argument-def> [, <argv-source-array>]);

argument-def

パラメータの定義を行う。

単一の文字列、または、文字列配列、または、パラメータ定義オブジェクトの配列を指定できる。

1) 文字列の場合

パラメータのキー名称をスペースで区切った文字列

例)

"inputFilePath outputFilePath"

2) Array of string

各要素がパラメーター名である文字列の配列。

例)

["inputFilePath", "outputFilePath"]

3) Array of definition object

このフォーマットでは、省略時のデフォルト値を指定できます。

デフォルト値が指定されていない場合は、nullが使用されます。

[
    {"name":"inputFilePath"},
    {
        "name"      : "outputFilePath",
        "default"   : "out.json"
    }
]

argv-source-array (省略可能)

コマンドラインパラメータを保持する文字列配列。

省略時はprocess.argvが使われます。

※ 注意:v0.0.2以前では、getの第二引数に、process.argvを与えると誤動作します。

定義より多くパラメータが指定されたら

v0.0.3以上では、パラメータ定義の数よりもコマンドラインで指定されたパラメーターが多い場合、未定義のパラメータが、返されるオブジェクトの空文字のキーに配列として保存されます(v0.0.2以前ではそれらは失われていました)。

args = require("hash-arg").get("inputFilePath outputFilePath");
console.log(JSON.stringify(args, null, "  "));

実行例:

$ node test/with-node-getopt.js input.json output.json foo bar
{
  "inputFilePath": "input.json",
  "outputFilePath": "output.json",
  "": ["foo", "bar"]
}

ライセンス

MIT

その他

単に利用するだけなら、npm install hash-argでOKですよ。


Node.jsで正常終了するとき process.exit(0) を呼んではいけない

f:id:takamints:20160402113544p:plain

Node.jsで正常終了するとき、 process.exit(0) を明示的に呼び出すべきではないってことを書いています。



関連記事:

takamints.hatenablog.jp

勢い余って呼んじゃった

Node.jsでコマンドラインのツールを書くとき、特に他の人に使ってもらうようなものだと特に、エラー処理をキチンと書くように努めています。

エラーメッセージを分かりやすく表示して、process.exit(-1) で、OSに対して異常終了を示すわけですが、勢い余って正常終了時に、ついつい process.exit(0) と書いてしまったのです。

その結果、「正常終了しているけれど、処理が正しくできてない」というよくわからない状況に困惑しました。

以下のようなコードです。

var fs = require("fs");
if(process.argv.length < 2) {
    console.error("エラー:ファイル名なし");
    process.exit(1);
}
fs.readFile(process.argv, "utf-8", function(err, data) {
    //
    //ファイルを読み込んであれこれする
    //
});
process.exit(0); //ここで正常終了ですよ

process.exitは「非同期処理を待ってくれない」

もうお気づきでしょうね最後の行です。

これだと readFileのコールバックが呼ばれる前に、プロセスが正常終了しちゃいます。

「nodeのプロセスは、全てのコールバックが終わってから終了する」けど、明示的にprocess.exitを呼び出したら、その時点で終了してしまいます。

これ当たり前。そうでなきゃ困るんだけど、意識していなかった。

呼び出さないほうがよい

ということで早速結論

Nodeのスクリプトで正常に処理を終了するときは、process.exitを呼び出さないほうが良い。

呼び出さなければ全部の処理を確実に実行してから、自動的に終了コード=0で終了してくれますが、 呼び出していると「これ、本当に全部の処理が行われてから正常終了しているの?」と疑うことが出来てしまいます。 コードレビューで指摘されると正統性を示すのが結構大変になりそうですし。

よく「呼び出す必要が無い」と説明されているのですが、もうチョット強めのニュアンスの方が良いのではないかと思います。 「呼び出さないほうがよい」ぐらいのほうが適当かと。

おまけ:0以外の終了コードで正常終了する方法

じゃあ0以外の終了コードで正常終了を表したい場合は、どうすればいいの?って思いますよね。

以下のようにすると良いですよ。

process.on("exit", function() {
    process.exit(1);
});

確認してみる。

$ node -e "process.on('exit', function(){process.exit(255);});"

$ echo $?
255

$

OKですね。


Nodeクックブック
Nodeクックブック
posted with amazlet at 16.04.02
David Mark Clements
オライリージャパン
売り上げランキング: 277,422

AWS Lambdaの関数アップロードをお手軽に

Lambda関数を、ローカルPCからアップロードするコマンドをご紹介。ダウンロードも出来ますよっと。

Lambda関数はAWSのWEBコンソールで編集できますが、数が増えるとやってらんない。そして外部モジュールを使う場合はWEBでは無理ですし。

でき得るならばローカルで、編集したのをアップロードしたいのだけど、その都度ZIPしてアップしてとか、それはそれで手間がかかってイライラします…

f:id:takamints:20160309213632p:plain
photo credit: 150120-N-ZZ999-002 via photopin (license)

てなことで、怒りに任せて(笑)取り急ぎ、勢いで3つばかり作ってしまいました。

その後、いろいろ更新してnpmとして公開してます。 最新版では、SQLライクな構文でDynamoDBを操作できるようになりました。 これについては以下の記事で説明してます。

takamints.hatenablog.jp


目次

  1. コマンド
    1. aws-lambda-get - Lambda関数をダウンロード
    2. aws-lambda-upload - Lambda関数を更新(上書き)
    3. aws-lambda-create - Lambda関数を新規作成
  2. 機能説明
    1. ローカルのLambda関数のディレクトリ構造
    2. アップロード前に特定の処理を実行する
  3. セットアップ
    1. 実行環境と必要なツール等
    2. インストール方法
    3. リポジトリ


aws-node-utilDynamoDB関連記事はこちらです:

takamints.hatenablog.jp


1. コマンド

1-1. aws-lambda-get - Lambda関数をダウンロード

AWSのLambda関数をダウンロードします。

$ aws-lambda-get <function-name>

パラメータ

  • function-name - Lambda関数名。

ちゃんとダウンロードできたら、カレントディレクトリに関数名のディレクトリが作られ、全てのファイルが展開されます。

安心してください、上書きしませんよ

既に同名のサブディレクトリがカレントディレクトリ直下にある場合はダウンロードしません。 ディレクトリの名前を変更して再実行してください。

1-2. aws-lambda-upload - Lambda関数を更新(上書き)

AWSのLambda関数をローカルファイルで更新します。

カレントディレクトリ直下の関数名のディレクトリをZIPファイルにまとめてAWSへアップロードして上書きます。

$ aws-lambda-upload <function-name>

パラメータ

  • function-name - Lambda関数名。カレントディレクトリ直下に同名のサブディレクトリが必要です。それをZIPしてアップします。

1-3. aws-lambda-create - Lambda関数を新規作成

ローカルのLambda関数をAWSへアップロードし、新規作成します。

カレントディレクトリ直下の関数名のディレクトリをZIPファイルにまとめてAWSへアップロードします。

$ aws-lambda-create <function-name> <role-arn>

パラメータ

  • function-name - Lambda関数名です。カレントディレクトリ直下に同名のサブディレクトリが必要です。それをZIPしてアップします。
  • role-arn - Lamdaに付けられるRoleのarnを指定してください。RoleはAWS IAMで作成出来ます。

Node.js限定です

新規作成に関しては、言語はnode.jsだけで、ハンドラーは index.handlerに固定されています。

生成だけなら別途awsコマンドでやってもたいした手間ではありません。その後、uploadすれば大丈夫なんじゃないかな?(試してないのでスミマセン)

2. 機能説明

2-1. ローカルのLambda関数のディレクトリ構造

AWSからダウンロードするとカレントディレクトリ直下に関数名のサブディレクトリが作成されます。 このディレクトリは、そのままアップロードできる状態になっています。

たとえば、以下のようにHelloLambdaという関数をダウンロードすると、 カレントディレクトリ直下にディレクトHelloLambdaが作成され、全ファイルが展開されます。

$ aws-lambda-get HelloLambda

以下は、HelloLambdaがモジュールを使用している場合の例です。

./HelloLambda/
    index.js
    node_modules/
      node-uuid/
        ....

2-2. アップロード前に特定の処理を実行する

アップロードする前に、Lambda関数が利用しているモジュールを更新したり、エディタのテンポラリファイルを削除したくなると思います。

各関数のディレクトリ直下に、.onupload.sh というファイルがあれば、アップロード前(正確にはZIPで固める前)に、これをシェルスクリプトとして実行します。

このファイル自体はAWSへはアップロードされません。

#node-uuidをインストール
if [ ! -d node_modules ]; then
    npm install node-uuid
else
    npm update node-uuid
fi
#VIMのバックアップファイルを消す
rm *~ .*~ 

3. セットアップ

3-1. 実行環境と必要なツール等

利用するには、以下のものが必要です。

  • AWSのアカウント - これがないと始まらない。今なら登録から1年間は基本的に無料。
  • AWSCLI - AWSコマンドラインインターフェースです。AWSからダウンロードしてインストールし、AWSアカウントで接続できるようにしておいてください。各コマンドから利用しています。
  • Bash - コマンドがシェルスクリプトなので必要です。
  • Node.js - JSONの処理等に利用しています。将来的には全部Nodeでやりたいです。
  • Zip/Unzipコマンド - Lambda関数のアップロード、ダウンロードで必要になります。(Bashで最初から利用できるのかも?)

3-2. インストール方法

npmにpublishしたので、インストールは npm install -g aws-node-utilでOKです。

インストールスクリプトINSTALL.sh

$ sh INSTALL.sh

実行すると、各スクリプト~/binへコピーし、node_modules以下のファイルを ~/node_modules へコピーしています。

かなりいい加減なインストールスクリプトで申し訳ないです。当方MinGW使ってるのでこれで済んでいます。

可能ならば、Forkして適切に修正してください。そしてプルリク下さい。よろしくお願いいたします。

3-3. リポジトリ

ソースは全て以下のGitHubに置いています。 ご自由に利用なさってください(MIT license)。 他にも雑多なスクリプトが入っていますが、それはそれでまたおいおい。

github.com

AWS LambdaからDynamoDBをQueryする

f:id:takamints:20160223124534p:plain
photo credit: Violet electric lightning versus turquoise storm - 09072012 via photopin (license)

JavaScript(Node.js)で記述した AWS Lambda から AWS-SDK を使って DynamoDBのテーブルをQueryするサンプルコードをご紹介。

ところでAWSAPIのリファレンスって検索しにくいですよねえ。 Lambdaがいろんな言語に対応しているため、REST APIと各言語向けの説明が別れていてチュートリアルも検索結果にたくさん出てきます。

※ 本記事ではDynamoDBのQueryするときのパラメータを説明します。 関連する記事として、検索条件を入力してQueryパラメータを生成するWebアプリは以下になります。合わせて参照してみてください。

takamints.hatenablog.jp

さらに、 DynamoDBをSQL(的な文)で扱う方法はコチラに書いています。

takamints.hatenablog.jp

DynamoDBのAPIは「Welcome Amazon DynamoDB」で説明されていますが、こちらはREST APIの説明で、各言語から呼び出せるメソッドについて直接記述されているわけではありません。だから定型的な読み替えと試行錯誤と推測が必要です。

日本語に翻訳されている文書もありますが、おおかた機械翻訳のようでして、かなり不可解な表現があったりします。 はなから英語で読んだほうが理解しやすいところもあったり。

などと、愚痴ってばかりでは始まらないので、ここはひとつ怒りに任せて(笑)Node.jsのLambda関数からDynamoDBを操作するサンプルコードをご紹介。

※ ここではAmazon DynamoDBAWS Lambdaについて、ひと通り基本的な知識を持っているって前提で書いてます。ワタシもAWS初心者なんですけどエラそうで申し訳ない。
※ フロントエンドは Amazon API Gatewayを利用している前提です。このため、実行結果はcontext.doneでJSON文字列を出力しています。


AWSエキスパート養成読本[Amazon Web Servicesに最適化されたアーキテクチャを手に入れる! ] (Software Design plus)
吉田 真吾 今井 智明 大瀧 隆太 松井 基勝 冨永 善視 藤原 吉規 大栗 宗
技術評論社
売り上げランキング: 1,300

DynamoDB API Query - キー属性で項目を検索

DynamoDBのテーブルから検索条件に合致した項目を問い合わせるためには、Query を使用します。条件無しで全項目を得るには、Queryではなく Scan を使用します。

Queryの検索条件には、少なくともプライマリーキーの条件が指定されなくてはならず、これに加えて、プライマリーソートキーやセカンダリーインデックスの条件も AND / OR で連結して記述できます。

さらに、プライマリーキー以外の項目によるフィルター条件も指定できますが、これは、あくまでも上記のプライマリーキーによる検索結果を得た後に対して絞りこむものですから、クエリ自体のパフォーマンスには、ほぼ無関係でしょう。(レスポンスが少なくなるので、通信速度には影響すると思われます)

サンプルコード(Node.js)

以下のように少しややこしい。初めて見たら軽くめまいを覚えそうです。

var AWS = require('aws-sdk');
var dynamo = new AWS.DynamoDB({
    region: 'ap-northeast-1'
});
var tableName = "myTable";
exports.handler = function(event, context) {

    //パラメータ定義
    var params = {
        "TableName": tableName,
        //キー、インデックスによる検索の定義
        "KeyConditionExpression":
            "id = :Id AND #Ts BETWEEN :T0 AND :T1",
        //プライマリーキー以外の属性でのフィルタ
        "FilterExpression":
            "#Fn &gt; :Fn",
        //属性名のプレースホルダの定義
        "ExpressionAttributeNames": {
            "#Ts": "timestamp",
            "#Fn": "value.floor"
        },
        //検索値のプレースホルダの定義
        "ExpressionAttributeValues": {
            ":Id" : { "S" : event.id },
            ":T0" : { "S" : event.T0 },
            ":T1" : { "S" : event.T1 }
            ":Fn" : { "S" : event.Fn }
        }
    };
    //クエリ実行
    dynamo.query(params, function(err, data) {
        if (err) {
            context.fail(
                new Error("Fail. err:" + err));
        } else {
            var response = JSON.stringify(data);
            context.done(null, response);
        }
    });
};

ポイントは、params。 ここではテーブルの指定以外に以下の3つのことをやっています。

  1. プライマリーキーによる検索を検索値のプレースホルダを使用して定義する
  2. プライマリーキー以外の属性で、キーの検索結果をフィルタする
  3. 属性名が予約語とかち合った場合は属性名のプレースホルダーで解決する

以降で、これらパラメータについて、詳細に説明します。

パラメータ

パラメータは第一引数に、objectとして与えられます。 下表に、ここで使用しているパラメータの概要です。

No. キー 説明
1 TableName string テーブル名を指定する。
2 KeyConditionExpression string キー属性での検索定義
3 ExpressionAttributeValues object 検索値のプレースホルダを定義する。
4 ExpressionAttributeNames object 属性名のプレースホルダを定義する。
5 FilterExpression string 検索後のフィルター条件を記述します。
  • 1~3は必須のはずです。 APIの説明ではTableNameだけ必須と書いてありますが、QUERYでは少なくともKeyConditionExpressionExpressionAttributeValuesを指定しないといけません。
  • 「4. ExpressionAttributeNames」は、検索する属性名に予約語が使われている場合に、読み替えを行うため必要となるパラメータ。
  • 他のパラメータについては、Query - Amazon DynamoDBで御確認ください。

KeyConditionExpression - キー、インデックスによる検索を定義する

KeyConditionExpressionではテーブル作成時に指定したキーによる検索条件を記述します。キー以外の条件は使えません。キー以外で絞り込みたい場合は、フィルターを使用する必要があります(フィルターはキーに関する制約を除けばKeyConditionExpressionと同じように条件を記述します)。

DynamoDBのテーブルのキーはパーティションキーとレンジキーの二種類があり、このうちパーティションキーは必ず検索条件に指定しなければなりません。レンジキーはオプションです。

検索条件の値は直接記述できません。値は検索値のプレースホルダを使って記述しなければなりません。 検索値のプレースホルダは、:(コロン)で始まる名称です。値に対する仮引数みたいなものですね。 具体的な値をプレースホルダに紐づけるには、次項で説明する ExpressionAttributeValues を使用します。

以下の :placeholderName が検索値のプレースホルダ

{
    "KeyConditionExpression":
        "keyAttrName = :placeholderName",
}

KeyConditionExpression についてまとめると以下のようになります。

ExpressionAttributeValues - 検索値のプレースホルダを定義する

ExpressionAttributeValuesには、KeyConditionExpressionやFilterExpressionに記述されているプレースホルダの値を型とともに指定します。

{
    "KeyConditionExpression":
        "keyAttrName = :placeholderName",
    "FilterExpression":
            "attrName > :placeholderName2",
    "ExpressionAttributeValues": {
        ":placeholderName":  { "S" : "11" },
        ":placeholderName2": { "N" : "12" }
    }
}

チュートリアルでは値だけが指定されていますが「structureじゃないとダメですよ」という例外が投入されます。

ExpressionAttributeNames - 属性名のプレースホルダを定義する

DynamoDBのテーブルの属性名に予約語が使われている場合、検索条件を記述できません(「属性名がキーワードだ」との例外投入) ので、ExpressionAttributeNamesで置き換えます。

以下の例では、予約語timestampと同じ属性名を、読み替えています。(#Tmの部分)

        "KeyConditionExpression": "#Tm = :T0",
        "ExpressionAttributeNames": {
            "#Tm": "timestamp"
        },
        "ExpressionAttributeValues": {
            ":T0" : { "S" : "" + event.t0 }
        }

他のAPI

テーブルの項目を操作するAPIは以下の様なものがあります。

  • 項目の問い合わせ - Query
  • 全項目取得 - Scan
  • 特定項目を取得 - GetItem
  • 項目上書き - PutItem
  • 項目変更 - UpdateItem
  • 項目削除 - DeleteItem
  • バッチ項目取得 - BatchGetItem
  • バッチ項目更新 - BatchWriteItem

  • PutItemとUpdateItemは紛らわしいけど、上書きしてしまうか、特定の属性の変更が行えるかの違いらしい(詳細不明)

  • バッチ系はちょっと使い勝手が違うかもしれませんが、一度にやっちゃう感じでしょう(詳細不明)
  • その他、テーブルを操作するAPIDynamoDBのAPIの説明ページ に書いてあります。

AWS関連記事

Node.js / npm 関連記事



その他、一般事項

Node.jsから呼び出すAPIのメソッド名

DynamoDBのAPIの説明ページ に掲載されている API の名称をキャメルケースに変換すれば、Node.jsのメソッド名になると思います(多分)。

DynamoDB APIの基本形

Node.jsでのDynamoDB APIは、以下のような形式で呼び出します。

dynamo.apiName(params, function(err, data) {
    if(err) {
        //エラー;
    } else {

        //dataにAPIの実行結果が入っています。

    }
});

(↑)apiNameをAPIの名前に書き換え。


RDB技術者のためのNoSQLガイド
渡部 徹太郎 河村 康爾 北沢 匠 佐伯 嘉康 佐藤 直生 原沢 滋 平山 毅 李昌 桓
秀和システム
売り上げランキング: 40,238

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