Open3

「Isolate」を勉強するスレット

Ryo24Ryo24

https://medium.com/flutter-jp/isolate-a3f6eab488b5

  • Dartはシングルスレッド/イベントループ
    • Main isolate: 通常の処理はすべてこのシングルスレッド上で実行する
    • キューに溜まったイベントが順に実行する
  • Futures&StreamsAPIなどは並行処理できるが、並列処理できない
    • シングルスレッド(Main Isolate)上での並行処理
    • ネットワーク通信のレスポンス待ちなどCPU負荷が低い処理は、並行処理(Futures/Streams)で良い
  • CPU負荷の高い処理は、Isolateを起動する
    • Isolate を直接扱うとコードが汚い。そのため、compute関数(Isolateの処理をラップ)で済む場合、優先的に使う
    • Main以外のIsolateを起動し、複数のIsolateで処理する = Dart版のマルチスレッドを用いた並列処理
    • スイッチングコスト(Isolate間の通信時間)が発生する。
  • Isolateの特徴
    • それぞれがイベントループを持つ
    • Isolate同士でメモリーを共有することはできず、データはメッセージよって交換する
    • プラットフォーム毎に実態は異なる
Ryo24Ryo24

https://api.dart.dev/stable/2.15.1/dart-isolate/Isolate-class.html

  • Dartの孤立した実行コンテキスト
  • コードはIsolateで実行され、同じIsolateならクラスや値にアクセス可
  • 別Isolateの情報にアクセスする場合、(ReceivePort or sendPort で)ポートでデータ送信したらアクセス可
    • Isolateオブジェクトを送れないけど、制御ポートや機能は送れる
    • 送信されたデータをもとに新しいIsolateオブジェクトを作成可能
  • Main IsolateIsolate Aは異なる。そのため、Main IsolateIsolate Aをコントロールできる
  • Isolateの作成(spwan)に成功すると、新しいIsolateオブジェクト(制御ポートや能力)を受け取る(Isolate.Isolateコンストラクタで能力を制限可能)
  • Isolaeは個別のイベントループ
  • Isolate間でイベントループの制御、一時停止、エラー検知などが可能
Ryo24Ryo24

https://dart.dev/guides/language/concurrency

  • Isolateを作成すれば、マルチプロセッサコアでコードを並列実行可能
  • 複数のIsolateDart Natie platformにのみ対応。そのため、Web環境はWeb Workers APIを使う
  • マルチコアCPUを利用するためには、開発者は共有スレッドメモリを活用しなければならない。しかし、共有状態の同時実行はコードが複雑かつエラーが発生しやすい。
  • DartはIsolate内部で実行する(= スレッド内で直接実行されない)
    • Isolateは独自のヒープメモリを持つ
    • Isolate間でアクセスできない
    • 共有メモリがないため、ミューテックスロックを考える必要がない
    • DartはIsolateを活用するとプロセッサが割り当て可能だと割り当て、一度に複数の独立したタスクを実行可能
    • Isolateはスレッドもしくはプロセスのような概念。しかしメモリイベントループスレッドが独立しているから別物

MainIsolate

  • 大抵の場合、Isolateに関して考えなくていい
  • 多くの場合、MainIsolateで事足りる
    • async / awaitで非同期処理すればOK
  • 正常に動作するアプリの定義
    • 高速起動及び、イベントループを実行
    • 必要に応じて非同期操作を行い、キューに入れられた各イベントを迅速に応答

Isolateのライフサイクル

  • Isolateは実行系(main関数など)のDartコードから始まる
  • イベントの終了と同時にIsolateも終了する(= 削除される?)
  • イベントのリスナーが登録されている場合、Isolateはリスナーが終了するまで残る
  • 例 : Isolateのライフサイクル
    • 1.Run some codeでIsolateが始まる
    • 2.Optionally, ....でイベントのリスナーが登録される
    • リスナーが処理されるまでIsolateは居続ける
    • リスナーの処理終了後、Isolateは終了する

イベント処理

  • キュー(queue)は先着順(先入れ、先出し)で基本的に処理される
    • エンキュー(enqueue) : データを入れること。データは末尾に追加され、行列の長さは1増加
    • デキュー(dequeue) : データを取り出すこと。データの先頭から取り出し、行列の長さは1減少
  • イベント処理はmain()終了後のMainIsolateで実行する
  • 同期操作に時間がかかりすぎると、レスポンスしない可能性が生まれる
    • アプリがフリーズしているよに見える
    • とぎれとぎれのアニメーションになる(= 崩れたアニメーション)
    • UIがフリーズする

タスクの分離(Isolateの分離)

  • 大規模計算でUIが応答しない(バックグラウンドワーカーの)場合、別のIsolateにオフロード(= Isolateの分離)するのを検討する
  • 例 : 計算処理を別Isolateで実行する
    • Main isolateでmainが実行
    • 計算処理用にWoker isolate(計算用のIsolate)を作成し、処理を移譲する
    • Worker isolateで計算処理し、結果をMain isolateに返す。その後isolateのイベントループが終了する
    • Main isolateWorker isolateから結果を受け取り、イベントループを終了する
  • Isolateは1つのオブジェクトを配達(= 送信)することができる
    • 配達したオブジェクトが推移的に到達できるものでも可(= Isolate本体を配達できないが、配達したオブジェクトで別のIsolateは作成可)
    • オブジェクト全てを送信できない
    • 推移的に到達可能なオブジェクトが送信不能の場合、送信は失敗する
      • 例 : 送信できないケース
        • List<Object>の要素内に送信不可能なオブジェクトが1つでも含まれている場合、失敗する
        • Socketは送信できないため、失敗する
  • Isolateは独自のメモリがあり、Isolate間で共有しない。
    • Isolateを分離し、重たい処理(block)を実行すると他Isolateに悪影響を与えない

コード例

  • Flutterの場合、compute()を使うと簡単にタスクを分離できる
MainIsolateで実行される
void main() async {
  // Read some data.
  final jsonData = await _parseInBackground();

  // Use that data
  print('number of JSON keys = ${jsonData.length}');
}

// Spawns an isolate and waits for the first message
Future<Map<String, dynamic>> _parseInBackground() async {
  final p = ReceivePort();
  await Isolate.spawn(_readAndParseJson, p.sendPort);
  return await p.first;
}
WorkerIsolateで実行する
Future _readAndParseJson(SendPort p) async {
  final fileData = await File(filename).readAsString();
  final jsonData = jsonDecode(fileData);
  Isolate.exit(p, jsonData);
}

MainIsolate&(MainIsolateが生成した)WorkerIsolateでタスク分離の実装例。

  • WorkerIsolateは関数を実行し、終了する。
  • MainIsolateが終了する時にメッセージを送信する。
  • _parseInBackground()は、WorkerIsolateを生成(作成&開始)し、結果を返すコード
    1. WorkerIsolateを作成する前にReceivePort classを作成する。これにより、WorkerIsolateMainIsolateに結果を返すことができる。
    2. spawn<T> methodを呼び出す。このメソッドにより、WorkerIsolateは作成&開始する。第1引数の_readAndParseJson WorkerIsolateが実行する関数を指定し、第2引数のp.sendPortWorkerIsolateMainIsolateに送信するRecivePortを指定する。
    3. WorkerIsolateが生成されるとMainIsolateは結果を待つ。ReceivePort classStream classをラップしているため、firstdプロパティでWorkerIsolateが送信される単一のメッセージを取得する。
  • exit methodWorkerIsolateを同期的に終了し、データを送信する。