Googleが先日発表したプログラミング言語Dart。その特長の一つとして挙げられるIsolateが面白そうだったので、ちょっと特性を調べてみた。結論から言うと、なかなか気難しい機能だな、という印象。
- 何か意味のあるサンプルを作りたかったわけではなく、単に動作の性質が知りたかっただけです
- 実装は読んでないので多分に憶測混じりです
- dartlang.org上のDartboardでテストしています
DartのIsolateはErlangのプロセスに似ていると評価されることがあるが(確かに似たところもあるが)、使用感は全く別物と思った方が良いと思う。最も大きな違いは「receiveがブロックしない」つまり「メッセージを受信するまで待つ」ことができないこと。
class IsolateA extends Isolate {
main() {
port.receive((msg, replyTo) {
//受信したメッセージの処理
});
//ここに書いたコードはメッセージの受信を待たず実行される
print("hoge");
}
}
これが何を意味するかと言うと、メッセージを受信した後で実行したい処理は全てクロージャの中に入れて、イベント駆動で動作させる必要があるということ。複数のIsolateを扱ったり、処理の前後関係が発生したりするとかなり面倒なことになる。Dart組み込みのPromiseを駆使することが必須になるだろう(最後に例を載せています)。
それと別のIsolateにデータを送るには厳密にメッセージ送信しか手段がない。コンストラクタやプロパティ経由で値を送ってもIsolateインスタンスの中からは参照できない。
class IsolateB extends Isolate {
int x = 0;
main() {
port.receive((msg, replyTo) {
print(x + msg);
});
}
}
main() {
IsolateB iso = new IsolateB();
//ここで値を設定しても消える
iso.x = 5;
//表示されるのは 105 ではなく 100
iso.spawn().then((port) {
port.send(100);
});
}
では初期化が必要な場合はどうするのかというと、初期化専用のメッセージ構造を決めておいて、その中に値を込める。例えば配列や辞書を使って。
class IsolateC extends Isolate {
int x = 0;
main() {
port.receive((msg, replyTo) {
if(msg[0] == "init") {
x = msg[1];
} else {
print(x + msg[1]);
}
});
}
}
main() {
new IsolateC().spawn().then((port) {
port.send(["init", 5]);
port.send(["print", 100]);
});
}
こういったやり方はErlangでもお馴染みだけど、Erlangと違ってパターンマッチはないしapplyもないし文字列からメソッドを得ることもできないし、そもそも静的型付け思想と全然相容れないしで、はっきりいって、ひどい。このIsolateという機能がどうにもDart言語に最適化されていない、練り込みが足りない感じがするのはこの辺りだ。
そしてメッセージ送信を用いても、制約なくどんなオブジェクトでも送れるわけではない。リファレンスに説明がない&実装を読んでないので詳しい条件は不明だが、独自に定義したクラスのインスタンスは送れない。組み込みのRegExpやPromise等も駄目(Dartboardにはエラーすら出ない)。intやStringといった基本的な値の他には、ListとMapくらいしか送れないと考えた方が良いかもしれない。
class Xyz {
String toString() => "xyz";
}
class IsolateD extends Isolate {
main() {
port.receive((msg, replyTo) {
print(msg);
});
}
}
main() {
new IsolateD().spawn().then((port) {
//エラーすら出ずに無視される
port.send(new Xyz());
});
}
それはそれとして、新たに起動したIsolateに対してメッセージを送るサンプルは多いが、別のIsolateから送られたメッセージを”メインIsolate”で受け取る方法があまり紹介されていないので触れておくと、ReceivePortを普通にnewして、toSendPortで対応するSendPortを生成、sendの第二引数で送信元Isolateに通知すれば良い。
class EchoIsolate extends Isolate {
SendPort mainPort;
main() {
//第2引数でSendPortが送られてくる
port.receive((msg, replyTo) {
if(msg == "init") {
mainPort = replyTo;
} else {
mainPort.send("echo: " + msg);
}
});
}
}
main() {
ReceivePort p = new ReceivePort();
new EchoIsolate().spawn().then((port) {
port.send("init", p.toSendPort());
port.send("foo");
port.send("bar");
});
//echo: foo, echo bar
p.receive((msg, replyTo) => print(msg));
}
特定のIsolateと単発でやり取りする場合はsendの代わりにcallを使うとこの過程を自動化してくれる。
class EchoIsolate extends Isolate {
main() {
port.receive((msg, replyTo) {
replyTo.send("echo: " + msg);
});
}
}
main() {
new EchoIsolate().spawn().then((port) {
port.call("foo").receive((msg, replyTo) => print(msg));
});
}
また特例として、メッセージの中にReceivePortを入れると自動でSendPortに変換されるので、次のようにも書ける。
class EchoIsolate extends Isolate {
SendPort mainPort;
main() {
port.receive((msg, replyTo) {
if(msg is SendPort) {
mainPort = msg;
} else {
mainPort.send("echo: " + msg);
}
});
}
}
main() {
ReceivePort p = new ReceivePort();
new EchoIsolate().spawn().then((port) {
port.send(p);
port.send("foo");
port.send("bar");
});
//echo: foo, echo bar
p.receive((msg, replyTo) => print(msg));
}
最後にFizzBuzzを題材として、2つのIsolateを起動し、”メインIsolate”を含む3つをリング状に連結してメッセージを回す例を載せておく。main => Fizz => Buzz => main とメッセージが回って、mainでprintする。それぞれの起動と初期化が非同期になるので、盛大にPromiseを使うことになった。
class FizzBuzzIsolate extends Isolate {
static final MSG_INIT = "init";
Promise<SendPort> _nextPromise;
String _word;
int _divisor;
_senderFunc(i, s) => (SendPort nextPort) {
if(i % _divisor == 0) s += _word;
nextPort.send([i, s]);
};
static init(SendPort port, SendPort nextPort, String word, int divisor) =>
port.send([MSG_INIT, [word, divisor]], nextPort);
main() {
_nextPromise = new Promise<SendPort>();
port.receive((msg, replyTo) {
if(msg == null) {
port.close();
} else if(msg[0] === MSG_INIT) {
_word = msg[1][0];
_divisor = msg[1][1];
_nextPromise.complete(replyTo);
} else {
_nextPromise.then(_senderFunc(msg[0], msg[1]));
}
});
}
}
main() {
ReceivePort mainPort = new ReceivePort();
Promise<SendPort>
fizz = new FizzBuzzIsolate().spawn(),
buzz = new FizzBuzzIsolate().spawn(),
p = new Promise();
p.waitFor([fizz, buzz], 2);
p.then((val) {
FizzBuzzIsolate.init(fizz.value, buzz.value, "Fizz", 3);
FizzBuzzIsolate.init(buzz.value, mainPort.toSendPort(), "Buzz", 5);
});
_sender(i) => (port) => port.send([i, ""]);
for(int i=1; i<=100; ++i) fizz.then(_sender(i));
mainPort.receive((message, replyTo) {
print(message[1] == "" ? message[0] : message[1]);
});
}