no-image

Dartでマルチスレッド(マルチアイソレート)処理

Dartでマルチスレッド処理をしようとすると、白くて綺麗な公式ドキュメントやころころ変わるAPIの洗礼を受けて脳味噌が爆発するはめになる。たび重なる仕様変更のせいもあってか、有志の解説も最新の仕様に追いついていないことが多い。

この記事では、Dartにおけるマルチスレッドの概念と簡単な使い方について説明する。なおこの記事は2014年7月10日現在での最新バージョン(Dart1.5)に合わせて書かれている。

Isolate

Dartではスレッドを直接扱うことはできず、かわりにisolateという単位を用いる。isolateはスレッドと同様に並行処理を実現できるが、それぞれ独立したメモリ領域を持っており、互いに直接干渉することはできない。isolate同士の干渉はメッセージ通信のみで行われ、任意のオブジェクトをメッセージとして送受信することができる。概念としてはスレッドよりもプロセスに近いかもしれない。isolateのCPUへの割り振りはDartVMによって行われる。

dart_isolate

 

使い方

isolateを扱うには標準ライブラリのdart:isolateが必要になる。公式のAPIリファレンスが参考になるだろう。

起動

isolateの起動はIsolate.spawnメソッドで行うことができる。このメソッドを実行することで、第1引数で指定した関数をエントリポイントとするisolateが起動する。第2引数では起動したisolateに渡す初期メッセージを指定することができる。

import 'dart:isolate';

// 起動するisolateのエントリポイントとなる関数。
// トップレベル関数かstaticメソッドである必要がある。
void invokeIsolate(initialMessage) => print(initialMessage);

void main() {
  // isolateを起動する。
  Isolate.spawn(invokeIsolate, "initial message");
}

別のdartファイルをisolateとして起動したい場合はIsolate.spawnUriメソッドを使用する。この場合のエントリポイントはmain関数になる。

import 'dart:isolate';

void main() {
  // sub_isolate.dartファイルをisolateとして起動する。
  Isolate.spawnUri(new Uri.file("sub_isolate.dart"), [], "initial message");
}
void main(args, initialMessage) {
  print("invoked");
}

メッセージの送受信

メッセージの送受信にはReceivePortオブジェクトおよびSendPortオブジェクトが用いられ、これらは対になっている。SendPortオブジェクトへメッセージを送信すると、対応するReceivePortオブジェクトがメッセージを受信する。ReceivePortクラスはStreamインターフェイスを実装しているので、listenすることで受信したメッセージを処理することができる。

import 'dart:isolate';

void main() {
  // ReceivePortを取得する。
  var receivePort = new ReceivePort();
  // ReceivePortに対応するSendPortを取得する。
  var sendPort = receivePort.sendPort;

  // メッセージ受信時のコールバック関数を指定。
  receivePort.listen((message)=>print(message));

  // 送信ポートにメッセージを送信。
  sendPort.send("hello");
}

isolate同士で通信するにはメッセージを送る対象のSendPortが必要なので、一般にisolate起動時の初期メッセージとしてSendPortを渡す場合が多い。

サンプル

以下はサブisolateからメインisolateへメッセージを送信し、メインisolateは受信したメッセージを表示するだけの簡単なサンプルである。ここではメッセージとしてStringオブジェクトを送信しているが、他のオブジェクトも同様に送信できる。

import 'dart:isolate';

/*
 * マルチisolateの簡単なサンプル。
 * 
 * サブisolateがメインisolateにメッセージを送信し、
 * メインisolateは受け取ったメッセージを表示する。
 * 
 * メインisolateが"bye"を含むメッセージを受け取ったら
 * 受信ポートを閉じる。
 */
void main() {
  // 現在のisolateでのReceivePortを取得。
  var receivePort = new ReceivePort();
  
  // ReceivePortへメッセージを送るための
  // SendPortを取得する。
  var sendPort = receivePort.sendPort;
  
  // メッセージ受信時のコールバック関数を指定。
  // ReceivePortはStreamインターフェイスを実装しているので、
  // 取り扱いもそれに従う。
  receivePort.listen((message){
    if(message is! String) { return; }
    print(message);
    if(message.contains("bye")) { receivePort.close(); }
  });
  
  // Isolate.spawnでisolateを起動する。
  // 第2引数で初期メッセージを指定できる。
  // ここでは、このisolateのSendPortを送信している。
  // (向こうからメッセージを送信してもらうため)
  Isolate.spawn(invokeSubIsolate, sendPort);
}

// サブisolateのエントリポイントとして使用する関数。
// 引数で初期メッセージを受け取る。
void invokeSubIsolate(initialMessage) {
  // このisolateでのReceivePort。
  // 今回は使わない。
  var receivePort = new ReceivePort();
  
  if(initialMessage is SendPort) {
    // メインisolateのSendPortへメッセージを送信。
    // ここではStringオブジェクトを送信している。
    var sendPort = initialMessage;
    sendPort.send("Hi, there.");
    sendPort.send("Can you hear me?");
    sendPort.send("bye."); 
  }
}

実行結果

$ dart isolate_sample.dart
Hi, there.
Can you hear me?
bye.

ウェブ環境でのisolate

ウェブ環境においてもisolateはサポートされているが、現状あまり安定していないようなので触らない方がいいだろう。一応、現行の仕様(実装)について記しておく。

基本的にはCUIアプリと同様だが、ウェブ環境ではIsolate.spawnは使えず、Isolate.spawnUriを使うことになる。他にも、仕様なのか実装が追いついていないのかはわからないが、メインisolate以外ではprint関数が無視されるなど、いくつかの制限があるようなので気をつけよう。

isolateの起動にはdart:htmlspawnDomUriという関数も使うことができる。spawnDomUriは名前の通りDOMへのアクセスが許可されているisolateを起動することができる。なおdart2jsでは未実装のようだ。

以下にIsolate.spawanUriを使用した場合の簡単なサンプルを示す。

サンプル

import 'dart:html';
import 'dart:isolate';

void main() {
  var receivePort = new ReceivePort();
  var sendPort = receivePort.sendPort;

  // Isolate.spawnは使用できない。
  Isolate.spawnUri(new Uri.file("sub_isolate.dart"), [], sendPort);
  
  receivePort.listen((message) {
    if(message is String) {
      var element = new DivElement()..text = message;
      document.body.append(element);
    }
  });
}
import 'dart:isolate';

void main(args, initialMessage) {
  if(initialMessage is SendPort) {
    var sendPort = initialMessage;
    sendPort.send("success");
  }
}