目次

ゾーン

非同期動的エクステント

#

この記事では、dart:async ライブラリのゾーン関連APIについて、トップレベルの runZoned() 関数と runZonedGuarded() 関数に焦点を当てて説明します。この記事を読む前に、Futureとエラー処理 で説明されているテクニックを確認してください。

ゾーンを使用すると、次のタスクを実行できます。

  • **キャッチされない例外によってアプリが終了しないように保護する**。たとえば、単純なHTTPサーバーは、次の非同期コードを使用する場合があります。

    dart
    runZonedGuarded(() {
      HttpServer.bind('0.0.0.0', port).then((server) {
        server.listen(staticFiles.serveRequest);
      });
    },
    (error, stackTrace) => print('Oh noes! $error $stackTrace'));

    HTTPサーバーをゾーン内で実行すると、サーバーの非同期コードでキャッチされない(ただし致命的ではない)エラーが発生しても、アプリは実行を継続できます。

  • **データ**(*ゾーンローカル値*と呼ばれる)**を個々のゾーンに関連付ける**。

  • **コードの一部または全部で、**`print()`** や **`scheduleMicrotask()`** などの限られたメソッドのセットをオーバーライドする**。

  • **コードがゾーンに出入りするたびに操作を実行する**。このような操作には、タイマーの開始または停止、スタックトレースの保存などが含まれます。

他の言語でゾーンと似たようなものに出くわしたことがあるかもしれません。Node.jsの*ドメイン*は、Dartのゾーンのインスピレーションとなりました。Javaの*スレッドローカルストレージ*にもいくつかの類似点があります。最も近いのは、Brian FordによるDartゾーンのJavaScriptポートであるzone.jsで、このビデオで説明されています。

ゾーンの基本

#

*ゾーン*は、呼び出しの非同期動的エクステントを表します。これは、呼び出しの一部として実行される計算と、そのコードによって登録された非同期コールバックを推移的に表します。

たとえば、HTTPサーバーの例では、`bind()`、`then()`、および`then()`のコールバックはすべて、`runZoned()`を使用して作成された同じゾーンで実行されます。

次の例では、コードは3つの異なるゾーンで実行されます:ゾーン#1(ルートゾーン)、ゾーン#2、およびゾーン#3

import 'dart:async';

main() {
  foo();
  var future;
  runZoned(() {          // Starts a new child zone (zone #2).
    future = new Future(bar).then(baz);
  });
  future.then(qux);
}

foo() => ...foo-body...  // Executed twice (once each in two zones).
bar() => ...bar-body...
baz(x) => runZoned(() => foo()); // New child zone (zone #3).
qux(x) => ...qux-body...

次の図は、コードの実行順序と、コードが実行されるゾーンを示しています。

illustration of program execution

`runZoned()`を呼び出すたびに新しいゾーンが作成され、そのゾーンでコードが実行されます。そのコードがbaz()の呼び出しなどのタスクをスケジュールすると、そのタスクはスケジュールされたゾーンで実行されます。たとえば、qux()の呼び出し(main()の最後の行)は、ゾーン#2で実行されるfutureにアタッチされていても、ゾーン#1(ルートゾーン)で実行されます。

子ゾーンは親ゾーンを完全に置き換えるわけではありません。代わりに、新しいゾーンは周囲のゾーン内にネストされます。たとえば、ゾーン#2にはゾーン#3が含まれ、ゾーン#1(ルートゾーン)にはゾーン#2ゾーン#3の両方が含まれます。

すべてのDartコードはルートゾーンで実行されます。コードは他のネストされた子ゾーンでも実行される可能性がありますが、少なくとも常にルートゾーンで実行されます。

キャッチされないエラーの処理

#

ゾーンは、キャッチされないエラーをキャッチして処理できます。

*キャッチされないエラー*は、多くの場合、`throw`を使用して例外を発生させるコードに、それを処理するための`catch`ステートメントが伴っていないために発生します。キャッチされないエラーは、Futureがエラー結果で完了したが、エラーを処理するための対応する`await`がない場合に、`async`関数でも発生する可能性があります.

キャッチされないエラーは、それをキャッチできなかった現在のゾーンに報告されます。デフォルトでは、ゾーンはキャッチされないエラーに応答してプログラムをクラッシュさせます。独自の カスタム *キャッチされないエラーハンドラ* を新しいゾーンにインストールして、キャッチされないエラーを好きなようにインターセプトして処理できます.

キャッチされないエラーハンドラを持つ新しいゾーンを導入するには、`runZoneGuarded`メソッドを使用します。その`onError`コールバックは、新しいゾーンのキャッチされないエラーハンドラになります。このコールバックは、呼び出しがスローする同期エラーを処理します.

dart
runZonedGuarded(() {
  Timer.run(() { throw 'Would normally kill the program'; });
}, (error, stackTrace) {
  print('Uncaught error: $error');
});

キャッチされないエラー処理を容易にする他のゾーンAPIには、`Zone.fork``Zone.runGuarded`、および`ZoneSpecification.uncaughtErrorHandler` が含まれます.

上記のコードには、例外をスローする非同期コールバック(`Timer.run()`を介して)があります. 通常、この例外は処理されないエラーとなり、トップレベルに到達します(スタンドアロンのDart実行可能ファイルでは、実行中のプロセスが強制終了されます). ただし、ゾーン化されたエラーハンドラを使用すると、エラーはエラーハンドラに渡され、プログラムはシャットダウンされません.

try-catchとゾーン化されたエラーハンドラの1つの顕著な違いは、ゾーンはキャッチされないエラーが発生した後も実行を継続することです. ゾーン内で他の非同期コールバックがスケジュールされている場合、それらは引き続き実行されます. その結果、ゾーン化されたエラーハンドラが複数回呼び出される可能性があります.

キャッチされないエラーハンドラを持つゾーンは、*エラーゾーン*と呼ばれます. エラーゾーンは、そのゾーンの子孫で発生したエラーを処理する場合があります.単純なルールにより、future変換のシーケンス(`then()`または`catchError()`を使用)でエラーが処理される場所が決まります。Futureチェーンのエラーは、エラーゾーンの境界を越えることはありません.

エラーがエラーゾーンの境界に達した場合、その時点で処理されないエラーとして扱われます.

例:エラーはエラーゾーンに侵入できません

#

次の例では、最初の行で発生したエラーは、エラーゾーンに侵入できません.

dart
var f = new Future.error(499);
f = f.whenComplete(() { print('Outside of zones'); });
runZoned(() {
  f = f.whenComplete(() { print('Inside non-error zone'); });
});
runZonedGuarded(() {
  f = f.whenComplete(() { print('Inside error zone (not called)'); });
}, (error) { print(error); });

例を実行すると、次の出力が表示されます.

Outside of zones
Inside non-error zone
Uncaught Error: 499
Unhandled exception:
499
...stack trace...

`runZoned()`または`runZonedGuarded()`の呼び出しを削除すると、次の出力が表示されます.

Outside of zones
Inside non-error zone
Inside error zone (not called)
Uncaught Error: 499
Unhandled exception:
499
...stack trace...

ゾーンまたはエラーゾーンを削除すると、エラーがさらに伝播することに注意してください.

スタックトレースが表示されるのは、エラーがエラーゾーンの外で発生するためです. コードスニペット全体をエラーゾーンで囲むと、スタックトレースを回避できます.

例:エラーはエラーゾーンから抜け出せません

#

上記のコードが示すように、エラーはエラーゾーンに侵入できません. 同様に、エラーはエラーゾーンから*抜け出す*こともできません. 次の例を考えてみましょう.

dart
var completer = new Completer();
var future = completer.future.then((x) => x + 1);
var zoneFuture;
runZonedGuarded(() {
  zoneFuture = future.then((y) => throw 'Inside zone');
}, (error) { print('Caught: $error'); });

zoneFuture.catchError((e) { print('Never reached'); });
completer.complete(499);

futureチェーンが`catchError()`で終わっても、非同期エラーはエラーゾーンを離れることができません. `runZonedGuarded()`にあるゾーン化されたエラーハンドラがエラーを処理します. その結果、*zoneFutureは完了しません* — 値もエラーもありません.

ストリームでのゾーンの使用

#

ゾーンとストリームのルールは、futureよりも単純です.

このルールは、ストリームがリッスンされるまで副作用があってはならないというガイドラインに従っています. 同期コードにおける同様の状況は、値を要求するまで評価されないIterableの動作です.

例:`runZonedGuarded()`でストリームを使用する

#

次の例では、コールバック付きのストリームを設定し、`runZonedGuarded()`を使用してそのストリームを新しいゾーンで実行します.

dart
var stream = new File('stream.dart').openRead()
    .map((x) => throw 'Callback throws');

runZonedGuarded(() { stream.listen(print); },
         (e) { print('Caught error: $e'); });

`runZonedGuarded()`のエラーハンドラは、コールバックがスローするエラーをキャッチします. 出力は次のとおりです.

Caught error: Callback throws

出力結果が示すように、コールバックはリスニングゾーンに関連付けられており、map() が呼び出されたゾーンには関連付けられていません。

ゾーンローカル値の格納

#

複数の同時実行計算が互いに干渉するため静的変数を使用できなかった場合は、ゾーンローカル値の使用を検討してください。デバッグに役立つゾーンローカル値を追加できます。別のユースケースは、HTTPリクエストの処理です。ユーザーIDとその認証トークンをゾーンローカル値に格納できます。

runZoned()zoneValues 引数を使用して、新しく作成されたゾーンに値を格納します。

dart
runZoned(() {
  print(Zone.current[#key]);
}, zoneValues: { #key: 499 });

ゾーンローカル値を読み取るには、ゾーンのインデックス演算子と値のキーを使用します:[キー]。互換性のある operator == および hashCode 実装があれば、どのオブジェクトもキーとして使用できます。通常、キーはシンボルリテラルです:#識別子

キーがマップするオブジェクトを変更することはできませんが、オブジェクトを操作することはできます。たとえば、次のコードはゾーンローカルリストにアイテムを追加します。

dart
runZoned(() {
  Zone.current[#key].add(499);
  print(Zone.current[#key]); // [499]
}, zoneValues: { #key: [] });

ゾーンは親ゾーンからゾーンローカル値を継承するため、ネストされたゾーンを追加しても、既存の値が誤って削除されることはありません。ただし、ネストされたゾーンは親の値をシャドウできます。

例:デバッグログにゾーンローカル値を使用する

#

foo.txt と bar.txt の2つのファイルがあり、すべての行を出力したいとします。プログラムは次のようになります。

dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

Future splitLinesStream(stream) {
  return stream
      .transform(ASCII.decoder)
      .transform(const LineSplitter())
      .toList();
}

Future splitLines(filename) {
  return splitLinesStream(new File(filename).openRead());
}
main() {
  Future.forEach(['foo.txt', 'bar.txt'],
                 (file) => splitLines(file)
                     .then((lines) { lines.forEach(print); }));
}

このプログラムは機能しますが、ここで各行がどのファイルからのものかを知りたいと仮定し、splitLinesStream() にファイル名引数を追加できないとします。ゾーンローカル値を使用すると、返される文字列にファイル名を追加できます(新しい行が強調表示されています)。

dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

Future splitLinesStream(stream) {
  return stream
      .transform(ASCII.decoder)
      .transform(const LineSplitter())
      .map((line) => '${Zone.current[#filename]}: $line')
      .toList();
}

Future splitLines(filename) {
  return runZoned(() {
    return splitLinesStream(new File(filename).openRead());
  }, zoneValues: { #filename: filename });
}

main() {
  Future.forEach(['foo.txt', 'bar.txt'],
                 (file) => splitLines(file)
                     .then((lines) { lines.forEach(print); }));
}

新しいコードは、関数シグネチャを変更したり、splitLines() から splitLinesStream() にファイル名を渡したりしないことに注意してください。代わりに、ゾーンローカル値を使用して、非同期コンテキストで動作する静的変数に似た機能を実装します。

機能のオーバーライド

#

runZoned()zoneSpecification 引数を使用して、ゾーンによって管理される機能をオーバーライドします。引数の値は、ZoneSpecification オブジェクトであり、これを使用して次の機能をオーバーライドできます。

  • 子ゾーンのフォーク
  • ゾーンでのコールバックの登録と実行
  • マイクロタスクとタイマーのスケジューリング
  • キャッチされない非同期エラーの処理(runZonedGuarded() はこのためのショートカットです)
  • 出力

例:printのオーバーライド

#

機能をオーバーライドする簡単な例として、ゾーン内のすべての出力を無効にする方法を次に示します。

dart
import 'dart:async';

main() {
  runZoned(() {
    print('Will be ignored');
  }, zoneSpecification: new ZoneSpecification(
    print: (self, parent, zone, message) {
      // Ignore message.
    }));
}

フォークされたゾーン内では、print() 関数は、指定された出力インターセプターによってオーバーライドされ、メッセージは単に破棄されます。 print()scheduleMicrotask() や Timer コンストラクターなど)は現在のゾーン(Zone.current)を使用して処理を行うため、出力をオーバーライドできます。

インターセプターとデリゲートへの引数

#

出力の例が示すように、インターセプターは Zone クラスの対応するメソッドで定義されている引数に3つの引数を追加します。たとえば、Zone の print() メソッドには1つの引数があります:print(String line)。ZoneSpecification で定義されている print() のインターセプターバージョンには、4つの引数があります:print(Zone self, ZoneDelegate parent, Zone zone, String line)

3つのインターセプター引数は、常に他の引数の前に同じ順序で表示されます。

self
コールバックを処理するゾーン。
parent
親ゾーンを表す ZoneDelegate。これを使用して、操作を親に転送します。
zone
操作が発生したゾーン。一部の操作では、操作がどのゾーンで呼び出されたかを知る必要があります。たとえば、zone.fork(specification)zone の子として新しいゾーンを作成する必要があります。別の例として、scheduleMicrotask() を別のゾーンに委任する場合でも、元の zone がマイクロタスクを実行するゾーンでなければなりません。

インターセプターがメソッドを親に委任する場合、メソッドの親(ZoneDelegate)バージョンには、追加の引数が1つだけあります。zone は、元の呼び出しが発生したゾーンです。たとえば、ZoneDelegate の print() メソッドのシグネチャは print(Zone zone, String line) です。

インターセプト可能な別のメソッド scheduleMicrotask() の引数の例を次に示します。

| **定義場所** | **メソッドシグネチャ** | | Zone | void scheduleMicrotask(void f()) | | ZoneSpecification | void scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, void f()) | | ZoneDelegate | void scheduleMicrotask(Zone zone, void f()) |

例:親ゾーンへの委任

#

親ゾーンに委任する方法の例を次に示します。

dart
import 'dart:async';

main() {
  runZoned(() {
    var currentZone = Zone.current;
    scheduleMicrotask(() {
      print(identical(currentZone, Zone.current));  // prints true.
    });
  }, zoneSpecification: new ZoneSpecification(
    scheduleMicrotask: (self, parent, zone, task) {
      print('scheduleMicrotask has been called inside the zone');
      // The origin `zone` needs to be passed to the parent so that
      // the task can be executed in it.
      parent.scheduleMicrotask(zone, task);
    }));
}

例:ゾーンに出入りする際のコードの実行

#

一部の非同期コードの実行にどれだけの時間がかかるかを知りたいとします。これは、コードをゾーンに配置し、ゾーンに入るたびにタイマーを開始し、ゾーンを離れるたびにタイマーを停止することで実行できます。

ZoneSpecification に run* パラメーターを提供すると、ゾーンが実行するコードを指定できます。

run* パラメーター(runrunUnary、および runBinary)は、ゾーンがコードの実行を要求されるたびに実行するコードを指定します。これらのパラメーターは、それぞれ、ゼロ引数、1引数、および2引数のコールバックに対して機能します。 run パラメーターは、runZoned() の呼び出し直後に実行される初期の同期コードにも機能します。

run* を使用したプロファイリングコードの例を次に示します。

dart
final total = new Stopwatch();
final user = new Stopwatch();

final specification = new ZoneSpecification(
  run: (self, parent, zone, f) {
    user.start();
    try { return parent.run(zone, f); } finally { user.stop(); }
  },
  runUnary: (self, parent, zone, f, arg) {
    user.start();
    try { return parent.runUnary(zone, f, arg); } finally { user.stop(); }
  },
  runBinary: (self, parent, zone, f, arg1, arg2) {
    user.start();
    try {
      return parent.runBinary(zone, f, arg1, arg2);
    } finally {
      user.stop();
    }
  });

runZoned(() {
  total.start();
  // ... Code that runs synchronously...
  // ... Then code that runs asynchronously ...
    .then((...) {
      print(total.elapsedMilliseconds);
      print(user.elapsedMilliseconds);
    });
}, zoneSpecification: specification);

このコードでは、各 run* オーバーライドはユーザータイマーを開始し、指定された関数を実行してから、ユーザータイマーを停止するだけです。

例:コールバックの処理

#

ZoneSpecification に register*Callback パラメーターを提供して、コールバックコード(ゾーンで非同期に実行されるコード)をラップまたは変更します。 run* パラメーターと同様に、register*Callback パラメーターには3つの形式があります。registerCallback(引数のないコールバックの場合)、registerUnaryCallback(1つの引数)、および registerBinaryCallback(2つの引数)です。

コードが非同期コンテキストに消える前に、ゾーンにスタックトレースを保存させる例を次に示します。

dart
import 'dart:async';

get currentStackTrace {
  try {
    throw 0;
  } catch(_, st) {
    return st;
  }
}

var lastStackTrace = null;

bar() => throw "in bar";
foo() => new Future(bar);

main() {
  final specification = new ZoneSpecification(
    registerCallback: (self, parent, zone, f) {
      var stackTrace = currentStackTrace;
      return parent.registerCallback(zone, () {
        lastStackTrace = stackTrace;
        return f();
      });
    },
    registerUnaryCallback: (self, parent, zone, f) {
      var stackTrace = currentStackTrace;
      return parent.registerUnaryCallback(zone, (arg) {
        lastStackTrace = stackTrace;
        return f(arg);
      });
    },
    registerBinaryCallback: (self, parent, zone, f) {
      var stackTrace = currentStackTrace;
      return parent.registerBinaryCallback(zone, (arg1, arg2) {
        lastStackTrace = stackTrace;
        return f(arg1, arg2);
      });
    },
    handleUncaughtError: (self, parent, zone, error, stackTrace) {
      if (lastStackTrace != null) print("last stack: $lastStackTrace");
      return parent.handleUncaughtError(zone, error, stackTrace);
    });

  runZoned(() {
    foo();
  }, zoneSpecification: specification);
}

例を実行してください。「最後のスタック」トレース(lastStackTrace)が表示されます。これには foo() が含まれています。foo() は同期的に呼び出されたためです。次のスタックトレース(stackTrace)は非同期コンテキストからのもので、bar() については知っていますが foo() については知りません。

非同期コールバックの実装

#

非同期 API を実装している場合でも、ゾーンをまったく扱う必要がない場合があります。たとえば、dart:io ライブラリが現在のゾーンを追跡することを期待するかもしれませんが、代わりに Future や Stream などの dart:async クラスのゾーン処理に依存しています。

ゾーンを明示的に処理する場合は、すべての非同期コールバックを登録し、各コールバックが登録されたゾーンで呼び出されるようにする必要があります。Zone の bind*Callback ヘルパーメソッドは、このタスクを容易にします。これらは register*Callbackrun* のショートカットであり、各コールバックが登録され、その Zone で実行されるようにします。

bind*Callback が提供するよりも多くの制御が必要な場合は、register*Callbackrun* を使用する必要があります。また、Zone の run*Guarded メソッドを使用することもできます。これは、呼び出しを try-catch でラップし、エラーが発生した場合に uncaughtErrorHandler を呼び出します。

まとめ

#

ゾーンは、非同期コードでキャッチされない例外からコードを保護するのに適していますが、それ以上のことができます。ゾーンにデータを関連付けることができ、出力やタスクスケジューリングなどのコア機能をオーバーライドできます。ゾーンは、より優れたデバッグを可能にし、プロファイリングなどの機能に使用できるフックを提供します。

その他のリソース

#
ゾーン関連の API ドキュメント
runZoned()runZonedGuarded()ZoneZoneDelegate、および ZoneSpecification のドキュメントを読んでください。
stack_trace
stack_trace ライブラリの Chain クラス を使用すると、非同期に実行されたコードのスタックトレースを改善できます。詳細については、pub.dev サイトの stack_trace パッケージ を参照してください。

その他の例

#

ゾーンを使用する、より複雑な例を次に示します。

task_interceptor の例
task_interceptor.dart の toy ゾーンは、scheduleMicrotaskcreateTimer、および createPeriodicTimer をインターセプトして、イベントループに yield せずに Dart プリミティブの動作をシミュレートします。
stack_trace パッケージのソースコード
stack_trace パッケージ は、ゾーンを使用して、非同期コードをデバッグするためのスタックトレースのチェーンを形成します。使用されるゾーン機能には、エラー処理、ゾーンローカル値、コールバックが含まれます。stack_trace GitHub プロジェクト で stack_trace ソースコードを見つけることができます。
dart:html と dart:async のソースコード
これら2つの SDK ライブラリは、非同期コールバックを備えた API を実装しているため、ゾーンを処理します。 Dart GitHub プロジェクトsdk/lib ディレクトリ で、ソースコードを参照またはダウンロードできます。

この記事のレビューをしてくれた Anders Johnsen と Lasse Reichstein Nielsen に感謝します。