Displaying posts tagged with

“Dart”

Dart言語のIsolateについて調べてみた

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