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]); }); }