Dartでマルチスレッド処理をしようとすると、白くて綺麗な公式ドキュメントやころころ変わるAPIの洗礼を受けて脳味噌が爆発するはめになる。たび重なる仕様変更のせいもあってか、有志の解説も最新の仕様に追いついていないことが多い。
この記事では、Dartにおけるマルチスレッドの概念と簡単な使い方について説明する。なおこの記事は2014年7月10日現在での最新バージョン(Dart1.5)に合わせて書かれている。
Isolate
Dartではスレッドを直接扱うことはできず、かわりにisolateという単位を用いる。isolateはスレッドと同様に並行処理を実現できるが、それぞれ独立したメモリ領域を持っており、互いに直接干渉することはできない。isolate同士の干渉はメッセージ通信のみで行われ、任意のオブジェクトをメッセージとして送受信することができる。概念としてはスレッドよりもプロセスに近いかもしれない。isolateのCPUへの割り振りはDartVMによって行われる。
使い方
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:htmlのspawnDomUriという関数も使うことができる。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");
}
}