Dart の並行性
このページでは、Dart での並行プログラミングの仕組みについての概念的な概要を説明します。イベントループ、非同期言語機能、およびアイソレートについて、高レベルで解説します。Dart での並行性の使用に関するより実践的なコード例については、非同期プログラミング ページおよび アイソレート ページをお読みください。
Dart における並行プログラミングとは、Future や Stream のような非同期 API と、プロセスを別個のコアに移動できる *アイソレート* の両方を指します。
すべての Dart コードはアイソレート内で実行され、デフォルトのメインアイソレートから開始し、明示的に作成した後続のアイソレートにオプションで拡張されます。新しいアイソレートをスパン(生成)すると、それは独自の分離されたメモリと独自のイベントループを持ちます。イベントループが、Dart での非同期および並行プログラミングを可能にしています。
イベントループ
#Dart のランタイムモデルはイベントループに基づいています。イベントループは、プログラムのコードの実行、イベントの収集と処理などを担当します。
アプリケーションの実行中、すべてのイベントは *イベントキュー* と呼ばれるキューに追加されます。イベントは、UI の再描画要求、ユーザーのタップやキーストローク、ディスクからの I/O など、何でもかまいません。アプリはイベントが発生する順序を予測できないため、イベントループはキューに入れられた順にイベントを 1 つずつ処理します。

イベントループの機能は、このコードに似ています。
while (eventQueue.waitForEvent()) {
eventQueue.processNextEvent();
}このイベントループの例は同期であり、単一のスレッドで実行されます。しかし、ほとんどの Dart アプリケーションは、一度に複数のことを行う必要があります。たとえば、クライアントアプリケーションは HTTP リクエストを実行しながら、ユーザーがボタンをタップするのを待つ必要がある場合があります。これを処理するために、Dart は Futures、Streams、および async-await のような多くの非同期 API を提供します。これらの API は、このイベントループを中心に構築されています。
たとえば、ネットワークリクエストを行う場合を考えてみましょう。
http.get('https://example.com').then((response) {
if (response.statusCode == 200) {
print('Success!');
}
}このコードがイベントループに到達すると、http.get の最初の節がすぐに呼び出され、Future が返されます。また、HTTP リクエストが解決されるまで then() 節のコールバックを保持するようにイベントループに指示します。それが完了したら、リクエストの結果を引数として渡して、そのコールバックを実行する必要があります。

この同じモデルが、一般的にイベントループが Dart の他のすべての非同期イベント(Stream オブジェクトなど)を処理する方法です。
非同期プログラミング
#このセクションでは、Dart の非同期プログラミングのさまざまなタイプと構文をまとめます。Future、Stream、async-await に既に慣れている場合は、アイソレートセクションに進むことができます。
Future
#Future は、最終的に値またはエラーで完了する非同期操作の結果を表します。
このサンプルコードでは、Future<String> の戻り値の型は、最終的に String 値(またはエラー)を提供する約束を表します。
Future<String> _readFileAsync(String filename) {
final file = File(filename);
// .readAsString() returns a Future.
// .then() registers a callback to be executed when `readAsString` resolves.
return file.readAsString().then((contents) {
return contents.trim();
});
}async-await 構文
#async および await キーワードは、非同期関数を宣言的に定義し、その結果を使用する方法を提供します。
ファイル I/O を待機中にブロックする同期コードの例を次に示します。
const String filename = 'with_keys.json';
void main() {
// Read some data.
final fileData = _readFileSync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
String _readFileSync() {
final file = File(filename);
final contents = file.readAsStringSync();
return contents.trim();
}同様のコードですが、非同期にするための変更(ハイライト表示)が加えられています。
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}main() 関数は _readFileAsync() の前に await キーワードを使用し、ネイティブコード(ファイル I/O)が実行されている間に他の Dart コード(イベントハンドラなど)が CPU を使用できるようにします。await を使用すると、_readFileAsync() が返す Future<String> が String に変換される効果もあります。その結果、contents 変数は暗黙的な型 String を持ちます。
次の図に示すように、Dart コードは readAsString() が Dart 以外のコード(Dart ランタイムまたはオペレーティングシステム)を実行している間一時停止します。readAsString() が値を返すと、Dart コードの実行が再開されます。

Stream
#Dart はストリームの形式でも非同期コードをサポートしています。ストリームは、将来的に繰り返し値を提供します。時間の経過とともに一連の int 値を提供する約束は、Stream<int> 型を持ちます。
次の例では、Stream.periodic で作成されたストリームは、1 秒ごとに新しい int 値を繰り返し発行します。
Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i);await-for と yield
#await-for は、ループの次の反復が新しい値が提供されるたびに実行される for ループの一種です。つまり、ストリームを「ループ処理」するために使用されます。この例では、引数として提供されたストリームから新しい値が発行されると、関数 sumStream から新しい値が発行されます。yield キーワードは、値のストリームを返す関数で return の代わりに、値のストリームを返す関数で使用されます。
Stream<int> sumStream(Stream<int> stream) async* {
var sum = 0;
await for (final value in stream) {
yield sum += value;
}
}async、await、Stream、および Future の使用についてさらに詳しく知りたい場合は、非同期プログラミングチュートリアルを参照してください。
Isolate
#Dart は、非同期 API に加えて、アイソレートによる並行性をサポートしています。ほとんどの最新デバイスにはマルチコア CPU が搭載されています。複数のコアを活用するために、開発者は共有メモリのスレッドを並行して実行することがあります。しかし、共有状態の並行性はエラーが発生しやすく、複雑なコードにつながる可能性があります。
スレッドの代わりに、すべての Dart コードはアイソレート内で実行されます。アイソレートを使用すると、Dart コードは複数の独立したタスクを同時に実行でき、利用可能な場合は追加のプロセッサコアを使用できます。アイソレートはスレッドやプロセスに似ていますが、各アイソレートは独自のメモリと、イベントループを実行する単一のスレッドを持ちます。
各アイソレートは独自のグローバルフィールドを持っており、アイソレートの状態が他のアイソレートからアクセスできないようにします。アイソレートはメッセージパッシングを介してのみ通信できます。アイソレート間に共有状態がないため、Dart では ミューテックスやロック、データ競合 のような並行性の複雑さは発生しません。とはいえ、アイソレートは競合状態を完全に防ぐわけではありません。この並行モデルの詳細については、アクターモデル についてお読みください。
メインアイソレート
#ほとんどの場合、アイソレートについて考える必要はありません。Dart プログラムはデフォルトでメインアイソレートで実行されます。これは、プログラムの実行が開始され、実行されるスレッドであり、次の図に示すとおりです。

単一アイソレートのプログラムでもスムーズに実行できます。これらのアプリは、次の行のコードに進む前に、async-await を使用して非同期操作の完了を待機します。適切に動作するアプリはすぐに起動し、できるだけ早くイベントループに到達します。その後、アプリはキューに入れられた各イベントに、必要に応じて非同期操作を使用して、迅速に応答します。
アイソレートのライフサイクル
#次の図に示すように、すべてのアイソレートは、main() 関数のような Dart コードの実行から始まります。この Dart コードは、イベントリスナーを登録する場合があります(たとえば、ユーザー入力やファイル I/O に応答するため)。アイソレートの初期関数が返されると、イベントを処理する必要がある場合、アイソレートはそのまま残ります。イベントを処理した後、アイソレートは終了します。

イベント処理
#クライアントアプリでは、メインアイソレートのイベントキューには、再描画要求、タップ通知、その他の UI イベントが含まれる場合があります。たとえば、次の図は、再描画イベント、タップイベント、その後の 2 つの再描画イベントを示しています。イベントループは、キューからイベントを先入れ先出し順に取得します。

イベント処理は、main() が終了した後にメインアイソレートで発生します。次の図では、main() が終了した後、メインアイソレートが最初の再描画イベントを処理します。その後、メインアイソレートはタップイベント、その後の再描画イベントを処理します。
同期操作が処理に時間がかかりすぎると、アプリは応答しなくなる可能性があります。次の図では、タップ処理コードに時間がかかりすぎるため、後続のイベントの処理が遅れます。アプリはフリーズしているように見え、実行するアニメーションがぎこちなくなる可能性があります。

クライアントアプリでは、長すぎる同期操作の結果は、しばしば ぎこちのない(スムーズでない)UI アニメーション になります。さらに悪いことに、UI が完全に応答しなくなる可能性があります。
バックグラウンドワーカー
#アプリの UI が、時間のかかる計算(たとえば、大きな JSON ファイルの解析)により応答しなくなった場合は、その計算をワーカーアイソレート(しばしば *バックグラウンドワーカー* と呼ばれます)にオフロードすることを検討してください。次の図に示す一般的なケースは、単純なワーカーアイソレートをスパンして計算を実行し、その後終了することです。ワーカーアイソレートは、終了時にメッセージで結果を返します。

ワーカーアイソレートは、I/O(たとえば、ファイルの読み書き)、タイマーの設定などを行うことができます。独自のメモリを持ち、メインアイソレートとは状態を共有しません。ワーカーアイソレートは、他のアイソレートに影響を与えることなくブロックできます。
アイソレートの使用
#ユースケースに応じて、Dart でアイソレートを操作するには 2 つの方法があります。
Isolate.run()を使用して、別個のスレッドで単一の計算を実行します。Isolate.spawn()を使用して、時間の経過とともに複数のメッセージを処理するアイソレート、またはバックグラウンドワーカーを作成します。長時間実行されるアイソレートの操作の詳細については、「アイソレート」ページをお読みください。
ほとんどの場合、Isolate.run はバックグラウンドでプロセスを実行するために推奨される API です。
Isolate.run()
#静的メソッド Isolate.run() は 1 つの引数を必要とします。それは、新しくスパンされたアイソレートで実行されるコールバックです。
int slowFib(int n) => n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
// Compute without blocking current isolate.
void fib40() async {
var result = await Isolate.run(() => slowFib(40));
print('Fib(40) = $result');
}パフォーマンスとアイソレートグループ
#アイソレートが Isolate.spawn() を呼び出すと、2 つのアイソレートは同じ実行可能コードを持ち、同じ *アイソレートグループ* に属します。アイソレートグループは、コードの共有などのパフォーマンス最適化を可能にします。新しいアイソレートは、アイソレートグループが所有するコードをすぐに実行します。また、Isolate.exit() は、アイソレートが同じアイソレートグループにある場合にのみ機能します。
特別なケースでは、新しいアイソレートを指定された URI のコードのコピーで設定する Isolate.spawnUri() を使用する必要がある場合があります。ただし、spawnUri() は spawn() よりもはるかに遅く、新しいアイソレートはスパンナーのアイソレートグループには属しません。別のパフォーマンス上の影響として、アイソレートが異なるグループにある場合、メッセージパッシングは遅くなります。
アイソレートの制限事項
#アイソレートはスレッドではありません
#マルチスレッドを持つ言語から Dart に移行した場合、アイソレートがスレッドのように動作することを期待するのは合理的かもしれませんが、そうではありません。各アイソレートは独自の状態を持っており、アイソレートの状態が他のアイソレートからアクセスできないようにします。したがって、アイソレートは独自のメモリへのアクセスによって制限されます。
たとえば、グローバルなミュータブル変数を持つアプリケーションがある場合、その変数はスパンされたアイソレートで別の変数になります。スパンされたアイソレートでその変数を変更しても、メインアイソレートでは変更されません。これがアイソレートの意図された機能であり、アイソレートの使用を検討する際には重要です。
メッセージのタイプ
#SendPort を介して送信されるメッセージは、ほとんどの Dart オブジェクトのタイプになりますが、いくつか例外があります。
Socketのようなネイティブリソースを持つオブジェクト。ReceivePortDynamicLibraryFinalizableFinalizerNativeFinalizerPointerUserTag@pragma('vm:isolate-unsendable')でマークされたクラスのインスタンス
これらの例外を除き、任意のオブジェクトを送信できます。詳細については、SendPort.send のドキュメントを参照してください。
Isolate.spawn() および Isolate.exit() は SendPort オブジェクトを抽象化しているため、同じ制限に従うことに注意してください。
アイソレート間の同期ブロッキング通信
#並列実行できるアイソレートの数には制限があります。この制限は、Dart でのメッセージを介した標準の *非同期* 通信には影響しません。数百のアイソレートを並行して実行し、進捗させることができます。アイソレートはラウンドロビン方式で CPU にスケジュールされ、頻繁に互いに譲り合います。
アイソレートは、純粋な Dart の外では、FFI を介した C コードを使用してのみ *同期* に通信できます。FFI 呼び出しで同期ブロッキングを使用してアイソレート間の同期通信を試みると、アイソレートの数が制限を超えている場合、特別な注意を払わないとデッドロックが発生する可能性があります。制限は特定の数にハードコーディングされているわけではなく、Dart アプリケーションが利用できる Dart VM ヒープサイズに基づいて計算されます。
この状況を回避するために、同期ブロッキングを実行する C コードは、ブロッキング操作を実行する前に現在のアイソレートを離れ、FFI 呼び出しから Dart に戻る前に再参加する必要があります。詳細については、Dart_EnterIsolate および Dart_ExitIsolate を参照してください。
Web 上での並行性
#すべての Dart アプリは、非ブロックでインターリーブされた計算のために async-await、Future、および Stream を使用できます。ただし、Dart Web プラットフォームはアイソレートをサポートしていません。Dart Web アプリは、Web Workers を使用して、アイソレートに似たバックグラウンドスレッドでスクリプトを実行できます。ただし、Web Workers の機能と能力はアイソレートとはいくらか異なります。
たとえば、Web Workers がスレッド間でデータを送信する場合、データをコピーして送受信します。ただし、特に大きなメッセージの場合、データのコピーは非常に遅くなる可能性があります。アイソレートも同様のことを行いますが、メッセージを保持するメモリをより効率的に *転送* する API も提供します。
Web Workers とアイソレートの作成方法も異なります。Web Workers は、別のプログラムエントリポイントを宣言し、それを個別にコンパイルすることによってのみ作成できます。Web Worker の起動は、Isolate.spawnUri を使用してアイソレートを起動するのと似ています。Isolate.spawn を使用してアイソレートを起動することもできます。これは、同じコードとデータを再利用するため、より少ないリソースで済みます。Web Workers には同等の API はありません。
追加リソース
#- 多くのアイソレートを使用している場合は、Flutter の
IsolateNameServer、または非 Flutter Dart アプリケーション向けの同様の機能を提供するpackage:isolate_name_serverを検討してください。 - Dart のアイソレートが基づいている アクターモデル についてさらに読む。
IsolateAPI に関する追加ドキュメント