Dart 並行処理と Isolate
並行処理とは、複数のタスクを同時に処理する能力のことです。
Dart の並行処理モデルは従来のマルチスレッドとは異なり、Isolate(分離領域)を使用して並列計算を実現します。
この章では、Dart の並行処理モデル、Isolate の概念と使い方、そしてメッセージ受け渡しの仕組みについて説明します。
Dart 並行処理モデル
ほとんどのプログラミング言語は、複数のスレッドが同じメモリ空間を共有する共有メモリ型のマルチスレッドモデルを採用しています。
このモデルの欠点は、競合状態(レースコンディション)やデッドロックが発生しやすいことです。
Dart は異なる戦略を採用しています:各 Isolate は独自の独立したメモリヒープを持ち、Isolate 間でメモリを共有しません。
Isolate 同士はメッセージ受け渡しで通信するため、データ競合の問題を根本的に回避できます。
Dart の並行処理モデルには3つの階層があります:
| 階層 | 仕組み | 適用シーン |
|---|---|---|
| イベントループ | シングルスレッド非同期(イベントループ) | I/O 操作、タイマー、ユーザー操作 |
| Isolate | 独立メモリ + メッセージ受け渡し | CPU 集中型の計算 |
| Future/Stream | 非同期プログラミングの文法 | ほとんどの日常的な開発シーン |
ほとんどの Dart プログラムの並行処理のニーズは、async/await と Future/Stream で満たせます。Isolate が必要になるのは、大量の CPU 集中型計算を行う場合だけです。Isolate を早い段階で導入するとコードが複雑になるだけで、得られるメリットは大きくありません。
Isolate の概念と使い方
Isolate は Dart の並行処理単位であり、各 Isolate は独自の独立したメモリとイベントループを持ちます。
メインプログラム自体も、ある Isolate(メイン Isolate)の中で実行されます。
Isolate.spawn で新しい Isolate を作成する
例
// この関数は新しい Isolate 内で実行される
// SendPort はメイン Isolate にメッセージを送信するために使用する
void heavyComputation(SendPort sendPort) {
print('新しい Isolate が計算を開始...');
// CPU 集中型の計算をシミュレート
int sum = 0;
for (int i = 1; i <= 10000000; i++) {
sum += i;
}
// 計算結果をメイン Isolate に送り返す
sendPort.send(sum);
print('新しい Isolate の計算が完了し、結果を送信しました');
}
Future<void> main() async {
print('メイン Isolate 起動');
// メッセージを受信するための ReceivePort を作成
var receivePort = ReceivePort();
// spawn で新しい Isolate を作成
await Isolate.spawn(heavyComputation, receivePort.sendPort);
print('メイン Isolate は結果を待つ間に他の処理ができます...');
// 新しい Isolate の計算結果を待つ
var result = await receivePort.first;
print('サンプル 計算結果: 1 から 10000000 の合計 = $result');
receivePort.close();
print('メイン Isolate 終了');
}
主 Isolate 启动 主 Isolate 在等待结果的同时可以做其他事情... 新 Isolate 开始计算... 新 Isolate 计算完成,结果已发送 サンプル 计算结果: 1 到 10000000 的和 = 50000005000000 主 Isolate 结束
比較:Isolate あり・なしのパフォーマンス差
例
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
void main() {
// メイン Isolate で計算——他のすべての操作をブロックする
var startTime = DateTime.now();
// 注意:n が大きすぎると、この計算は非常に時間がかかる
// 実際の開発では、このような計算は専用の Isolate に置くべき
var result = fibonacci(40);
var elapsed = DateTime.now().difference(startTime);
print('サンプル フィボナッチ(40) = $result');
print('所要時間: ${elapsed.inMilliseconds}ms');
print('(注意:計算中、メイン Isolate は他のタスクを処理できません)');
}
Flutter アプリでは、メイン Isolate で時間のかかる同期計算を実行すると、UI がフリーズします。解決策は、計算タスクを別の Isolate に置き、計算完了後にメッセージで結果を送り返して UI を更新することです。
メッセージ受け渡しの仕組み
Isolate 間では、SendPort と ReceivePort を介してメッセージを受け渡しします。
メッセージはシリアライズ可能なもの(基本型、String、List、Map など)でなければならず、関数やクロージャを渡すことはできません。
双方向通信
例
// 新しい Isolate 内で実行されるワーカー関数
void workerIsolate(SendPort mainSendPort) {
// メイン Isolate からのメッセージを受信するための独自の ReceivePort を作成
var workerReceivePort = ReceivePort();
// 最初にワーカーの SendPort をメイン Isolate に送信し、双方向チャネルを確立する
mainSendPort.send(workerReceivePort.sendPort);
print('ワーカー Isolate: タスク待機中...');
// メイン Isolate から送られてくるタスクを監視
workerReceivePort.listen((message) {
if (message is List<int>) {
// タスク受信:リスト内の各数値の2乗を計算
print('ワーカー Isolate: データ受信 $message');
var result = message.map((n) => n * n).toList();
// mainSendPort 経由で結果を送り返す
mainSendPort.send(result);
} else if (message == 'exit') {
print('ワーカー Isolate: 終了信号を受信、クローズ');
workerReceivePort.close();
mainSendPort.send('goodbye');
}
});
}
Future<void> main() async {
print('メイン Isolate: 起動');
// メインの受信ポートを作成
var mainReceivePort = ReceivePort();
// ワーカー Isolate を起動
await Isolate.spawn(workerIsolate, mainReceivePort.sendPort);
// ワーカーが自身の SendPort を送ってくるのを待つ(双方向通信を確立)
SendPort? workerSendPort;
await for (var msg in mainReceivePort) {
if (msg is SendPort) {
workerSendPort = msg;
print('メイン Isolate: ワーカーとの双方向通信を確立しました');
break;
}
}
// ワーカーの SendPort 経由でタスクを送信
workerSendPort!.send([1, 2, 3, 4, 5]);
workerSendPort.send([10, 20, 30]);
// ワーカーの計算結果を受信
int responseCount = 0;
await for (var msg in mainReceivePort) {
if (msg is List<int>) {
print('メイン Isolate: 結果を受信 $msg');
responseCount++;
if (responseCount == 2) break;
}
}
// 終了信号を送信
workerSendPort.send('exit');
await for (var msg in mainReceivePort) {
if (msg == 'goodbye') {
print(メイン Isolate: ワーカーが終了しました);
break;
}
}
mainReceivePort.close();
print(サンプル 双方向通信デモ終了);
}
主 Isolate: 启动 主 Isolate: 已建立与 Worker 的双向通信 Worker Isolate: 等待任务... Worker Isolate: 收到数据 [1, 2, 3, 4, 5] 主 Isolate: 收到结果 [1, 4, 9, 16, 25] Worker Isolate: 收到数据 [10, 20, 30] 主 Isolate: 收到结果 [100, 400, 900] Worker Isolate: 收到退出信号,关闭 主 Isolate: Worker 已退出 サンプル 双向通信演示结束
Isolate.run による簡略化(Dart 3.0+)
Dart 3.0 で Isolate.run() が導入され、1回限りの計算タスクの利用が大幅に簡略化されました。
例
// 時間のかかる計算関数
int complexCalculation(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i * i;
}
return result;
}
Future<void> main() async {
print('計算を開始...');
// Isolate.run:1行のコードで完了。Isolate の作成、実行、結果の返却を自動で行う
var result = await Isolate.run(() => complexCalculation(10000000));
print('サンプル 計算結果: $result');
print('計算完了');
// 比較:Isolate を使用しない場合
print('(メイン Isolate で直接計算すると、他の操作をブロックします)');
}
开始计算... サンプル 计算结果: 333333283333335000000 计算完成 (如果在主 Isolate 中直接计算,会阻塞其他操作)
Isolate.run() は「1回計算を実行して結果を返す」シナリオに適しています。継続的な双方向通信(長時間実行されるバックグラウンドサービスなど)が必要な場合は、Isolate.spawn() + SendPort/ReceivePort を使用する必要があります。
Isolate のメッセージ受け渡しの制限
| 渡すことができるもの | 渡せないもの |
|---|---|
| null、bool、int、double、String | 関数、クロージャ |
| List、Map、Set(要素も渡すことができます) | Stream、Future |
| SendPort(通信チェーンの確立に使用) | ほとんどの非基本型オブジェクト |
| Capability(権限トークン) | ファイルハンドル、ネットワークソケット |
| TransferableTypedData(大きなデータを効率的に受け渡し) | カスタムクラス(シリアライズ可能な場合を除く) |
並行処理モデルの比較
| 特性 | Dart Isolate | 従来のマルチスレッド(例:Java) |
|---|---|---|
| メモリモデル | 独立メモリ、共有しない | 共有メモリ |
| 通信方法 | メッセージ受け渡し(SendPort) | 共有変数 + ロック |
| データ競合 | 発生しない(共有なし) | ロック機構による保護が必要 |
| デッドロックのリスク | ほぼ発生しない | 発生する |
| 作成オーバーヘッド | 比較的大きい | 比較的小さい |
| 適用シーン | CPU集約型並列計算 | 汎用並行処理 |
その他の拡張DartのIsolateモデルはデータ競合を回避しますが、その代償として作成と通信のオーバーヘッドが大きくなります。何千ものIsolateを作成しないでください——通常は数個のIsolateで十分です。I/O集約型タスクには、async/awaitを使用するのが最適です。