Dart/Flutterのisolateを理解

isolate = dartのマルチスレッド的なやつ。

  • main()はmain isolate
  • 子のisolateを生成した親のisolateは子のisolateの終了を待つことができる
  • 親子isolate間でping pong的な相互通信もできる

worker isolateで1秒かかる処理を待つ

import 'dart:isolate';

void main() async {
  debugPrint("${DateTime.now()} start");
  final result = await _runInBackground();
  debugPrint("${DateTime.now()} $result");
  debugPrint("${DateTime.now()} end");
}

Future<String> _runInBackground() async {
  // 以下の3行はおまじないと思うことにする
  // 大事なのは_runInIsolateで実際のバックグラウンド処理をするということ
  final p = ReceivePort();
  await Isolate.spawn(_runInIsolate, p.sendPort); // worker isolateの生成
  return await p.first as String;
}

Future<void> _runInIsolate(SendPort p) async {
  // 1秒待つ。実際の実装ではここで時間のかかる処理をする。
  await Future.delayed(const Duration(seconds: 1));
  const result = "worker isolate終了";
  Isolate.exit(p, result);
}

これくらいならisolateを使うまでもないはず。次のような相互通信などで必要になる。

isolateで相互通信する

やること

  • mainのバックグラウンド処理からメッセージを投げると遅延してworker isolateから応答が来る
  • mainのバックグラウンド処理がもういいよとworker isolateに伝えるまで、そのやりとりが続く
  • mainは1回ごとのworker isolateの応答を受け取りprintする
import 'dart:isolate';
import 'package:async/async.dart';

void main() async {
  debugPrint("${DateTime.now()} start ping pong");
  // await forで通信が終わるまでループする
  await for (final message in _pingPongInBackground()) {
    debugPrint("${DateTime.now()} $message");
  }
  debugPrint("${DateTime.now()} end ping pong");
}

// 戻り値は、Stream<1回の通信で返す値の型>、お尻はasyncではなくasync*と書く
Stream<String> _pingPongInBackground() async* {
  final p = ReceivePort();
  await Isolate.spawn(_runInWorkerIsolate, p.sendPort);

  // 呼び出し元のawait forに順次値を返すStreamを生成
  final stream = StreamQueue<dynamic>(p);
  // 最初にworker isolateから通信するためのPortを受け取る(Aと対応)
  SendPort sendPort = await stream.next;

  // worker isolateとの相互通信を10回繰り返す
  for (var i = 0; i < 10; i++) {
    // pingをworker isolateに投げる
    sendPort.send("ping $i");
    // worker isolateの応答を待つ
    String pong = await stream.next;
    // Streamへの返却値はyieldで返す
    // main()のawait forで順次受け取る
    yield pong;
  }

  // worker isolateにもうpingは送らないから終了していいよと伝える
  sendPort.send(null);

  // Stream終了。main()のawait forループを終了させる
  await stream.cancel();
}

// worker isolateの処理
Future<void> _runInWorkerIsolate(SendPort p) async {
  final commandPort = ReceivePort();
  // 最初に自分と通信するためのPortを渡す(A)
  p.send(commandPort.sendPort);

  // ping待機ループ
  await for (final ping in commandPort) {
    if (ping is String) {
      // ping受信を検知
      // 1秒遅延。実際の実装ではここで通信処理など時間のかかる処理をする
      await Future.delayed(const Duration(seconds: 1));
      // 応答を返す
      p.send("pong ${DateTime.now()} for $ping");
    } else if (ping == null) {
      // 通信終了していいよと言われたらループを抜ける
      break;
    }
  }

  Isolate.exit();
}