LoginSignup
5
4

More than 5 years have passed since last update.

Zones (Dart公式ドキュメントの翻訳)

Last updated at Posted at 2014-12-21

原文: https://www.dartlang.org/articles/zones/
2014/12/14時点のものを翻訳
ライセンスはwww.dartlang.orgのものに従います
英語難しいので間違いあったらご指摘ください

Asynchronous dynamic extents (非同期の動的な拡張)

著者: Florian Loitsch と Kathy Walrath
2014年3月

この記事では dart:async ライブラリの zone 関連のAPIについて、トップレベル関数の runZoned() 関数に注目して説明します。この記事を読む前に、Futureとエラーハンドリング で取り上げられているテクニックについて知っておく必要があります。

現在最もzoneが使われているのは、非同期に実行されるコードによるエラーの、エラーハンドリングを目的としています。例えば、シンプルなHTTPサーバーの場合、次のように使用します。

runZoned(() {
  HttpServer.bind('0.0.0.0', port).then((server) {
    server.listen(staticFiles.serveRequest);
  });
},
onError: (e, stackTrace) => print('Oh noes! $e $stackTrace'));

zoneを有効にしてHTTPサーバーを実行すると、(致命的でない)エラーがをサーバーの非同期のコードでキャッチしていないにもかかわらず、アプリケーションは動作し続けます。

API note: この利用例は、常にzoneが必須なわけではありません。我々は、キャッチしてないエラーを拾う事ができるAPIをisolateが持つようになる事を期待しています。

zoneは次のようなタスクを可能にします:

  • 前述の例のように、非同期のコードから投げられた例外が原因で終了しないように、アプリケーションを保護します。
  • 個々のゾーンと、(zoneのローカルな)データを紐付けます。
  • 例えばprint()scheduleTask()といった、制限のあるメソッドを、一部、あるいは全てのコード上で上書きする事ができます。
  • コードがzoneに出たり入ったりする度に何らかの操作(例えばタイマーの開始や終了、スタックトレースの保存など)を実行する事ができます。

あなたは他の言語で、zone と似たものを見たことがあるのではないでしょうか。NodeJSの Domains はDartのzoneのインスピレーションになりました。Javaのスレッドローカルストレージはいくつかの類似点を持っています。このビデオで説明されている、Brian FordのDartのzoneのJavascriptへのポートであるzone.jsが一番似ています。

Zoneの基本

zoneは、呼び出しの非同期で動的な拡張を意味しています。それは部分的な呼び出しとして振る舞い、かつ、他動詞的にコードに登録されたコールバックを非同期で処理します。

例えばHTTPサーバーの例では、bind()then()や、then()のコールバックは全て、runZoned()で生成された、同じzone上で実行されます。

次の例では3つの異なるzoneでコードが実行されます。zone #1(ルートzone)と、zone #2と、zone #3の3つです。

Zones___Dart__Structured_web_apps-6.png

訳注:サンプルコード上の背景色に意味があり、Qiitaで表現できないので画像を貼り付けています。
青色が zone #1(ルートzone)、 緑色が zone #2黄色が zone #3 を表しています。

次の図ではコードの実行順序に加えて、どのzoneでコードが実行されるのかを表しています。

trace.png

runZoned()を呼び出す度に、新しいzoneが生成され、コードはそのzoneで実行されます。(baz()を呼び出すように)コードがタスクをスケジュールする時、そのタスクはスケジュールされたzoneで実行されます。例えば、(main()の最後の行の) qux() の呼び出しは、zone #2 で実行される future に、渡されているにも関わらず、zone #1 (ルートzone) で実行されます。

子のzoneが親のzoneを置き換えるわけではありません。むしろ、新しいzoneは周りのzoneの中にネストされています。例えば zone #2zone #3を含んでいますし、zone #1(ルートzone)はzone #2zone #3の両方を含んでいます。

全てのDartのコードはルートzoneで実行されます。他のネストした子のzoneの中で実行される事もありますが、それは少なくとも常にルートzoneで実行されています。

非同期のエラーをハンドリングする

zone でもとも利用される機能のひとつに、非同期に実行されるコードでcatchされてないエラーをハンドリングできる事があります。同期的に実行されるコードにおいてのtry-catchと似たコンセプトです。

catchされてないエラーは、catch句でハンドリングされてない例外をthrowさせるコードが原因で発生します。他には、new Future.error()を呼び出したり、CompleterのcompleteError()メソッドによって、非同期の例外を発生させる事ができます。

runZoned()onError 引数にzoneのエラーハンドラ(非同期エラーハンドラ)を設定する事で、そのzoneで発生したあらゆるcatchされてないエラーをハンドリングする事ができます。例えば:

runZoned(() {
  Timer.run(() { throw '普段はプログラムを殺します'; });
}, onError: (error, stackTrace) {
  print('catchしてないエラー: $error');
});

ここで示したコードには例外を発生させる(Timer.run()に渡してある)非同期のコールバックがあります。通常であればこの例外は、catchされてない例外であり、トップレベルに到達します(スタンドアローンなDart実行であれば、実行中のプロセスを殺します)。しかし、zoneのエラーハンドラによってこのエラーはエラーハンドラに渡され、プログラムをシャットダウンしません。

try-catchとzoneのエラーハンドラにおいて注目すべき違いは、catchsされないエラーが発生した後もzoneが実行を続けるという事です。そのzoneに他の非同期なコールバックがスケジュールされていた場合、それらは実行されます。つまり、zoneのエラーハンドラは複数回呼び出される可能性があります。

また、(エラーハンドラを持つ)zoneは、子(あるいは、その他の子孫)が起因のエラーを処理する可能性がある事に注意してください。どこでエラーが処理されるのかは単純なルールで、(then()catchError()を利用している)Futureの変換シーケンスで決定されます:

Futureのエラーが、エラーゾーンの境界を越えて連鎖する事はありません。

エラーがエラーゾーンの境界に到達した場合、その時点でハンドリングされてないエラーとして処理されます。

訳注:以下「エラーゾーン」という単語が出てきますが、ほとんどのケースで「onErrorが設定してあるzone」というニュアンスで使われているようです。

例:エラーはエラーゾーンを横断する事ができない

次の例では、最初の行で発生したエラーはエラーzoneを横断する事ができません。

var f = new Future.error(499);
f = f.whenComplete(() { print('runZonedの外側'); });
runZoned(() {
  f = f.whenComplete(() { print('エラーゾーンが無いzoneの内側'); });
});
runZoned(() {
  f = f.whenComplete(() { print('エラーゾーンが有るzoneの内側 (呼び出されない)'); });
}, onError: print);

実行時の出力は以下のようになるでしょう:

runZonedの外側
エラーゾーンが無いzoneの内側
Uncaught Error: 499
Unhandled exception:
499
...stack trace...

runZoned()onError引数を削除すると、次のような出力になるでしょう:

runZonedの外側
エラーゾーンが無いzoneの内側
エラーゾーンが有るzoneの内側 (呼び出されない)
Uncaught Error: 499
Unhandled exception:
499
...stack trace...

zoneかエラーゾーンのどちらかを削除すると、エラーがさらに伝わってゆくので注意してください。

エラーがエラーゾーンの外側で発生したため、スタックトレースが表示されます。コードスニペット全体にエラーゾーンを追加する事で、スタックトレースを回避する事ができます。

例:エラーはエラーゾーンを離れられない

前述のコードはエラーがエラーゾーンを横断する事ができない事を示しました。同様に、エラーはエラーゾーンの外に越える事ができません。次の例をじっくり見てみましょう:

var completer = new Completer();
var future = completer.future.then((x) => x + 1);
var zoneFuture;
runZoned(() {
  zoneFuture = future.then((y) => throw 'zoneの内側');
}, onError: (error) {
  print('エラーを拾いました: $error');
});
zoneFuture.catchError((e) { print('ここには来ません'); });
completer.complete(499);

訳注: 上記の実行結果は以下になります
エラーを拾いました: zoneの内側

FutureのチェーンはcatchError()で終わったが、非同期なエラーはエラーゾーンを離れられません。かわりに(onErrorが指定されている)zoneのエラーハンドラがエラーを処理します。結果、zoneFutureは完了していません。エラーでも、値でも。

訳注: 実行結果はエラーを拾いました: zoneの内側になります。
うまく翻訳できませんでしたが、言わんとしてるところは、runZoned()内のfutureのthenで発生した例外がzoneFuture.catchErrorに到達せずに、onErrorで終了しているという点が重要なポイントです。
また、Completerは、Futureを扱いやすくするラッパーみたいなクラスです。たぶん。

zoneをstreamと一緒に使う

zoneとstreamのルールは、Futureよりも簡単です:

変換(transformation)やその他のコールバックはstreamがlistenしてるzoneで実行されます。

このルールは「streamはlistenしてる間副作用を持つべきではない」というガイドラインに従ってます。同期的コードにおける似たような事に、「イテレータの、値を求められるまで評価しない振る舞い」といったものがあります。

例:streamをrunZoned()と一緒に使う

次にstreamをrunZoned()と一緒に使う例をあげます:

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

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

コールバックにより投げられた例外はrunZoned()のエラーハンドラによってcatchされます。出力は、次のようになります。

Caught error: Callback throws

この出力が示すように、コールバックは、map()が呼び出されているzoneではなく、listenしてるzoneに関連付けられています。

zoneのローカルバリューを保存する

static変数を利用したいが、複数の同時に実行される計算が干渉するなどの理由で利用できなかった場合、zoneのローカルバリューを使う事を検討してください。デバッグを助けるためにzoneローカルバリューを追加しても良いでしょう。他の利用例としては、HTTPリクエストで、zoneローカルバリューにあるユーザーIDと認証用のトークンを扱うなどです。

runZoned()zoneValues引数で、新しいzoneに値を保存する事ができます。

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

zoneローカル変数を参照するために、zoneのインデックス演算子と値のキー: [key] を利用します。キーはSymbolでなければなりません。通常は、キーはシンボルリテラル #identifier です。

API note: リビジョン34248のリリースになると、キーはシンボルに限定されなくなります。

キーに紐付けられたオブジェクトを変更する事はできませんが、オブジェクトを操作する事はできます。例えば次のコードは、zoneのローカルリストにアイテムを追加しています:

runZoned(() {
  Zone.current[#key].add(499);
  print(Zone.current[#key]); // 出力は「[499]」になります
}, zoneValues: { #key: [] });

zoneはzoneローカルバリューを親のzoneから継承し、ネストしたzoneが既存の値をうっかり失ったりはしません。ただ、ネストしたzoneは、親の値をシャドーイング(訳注:ASCII.jpデジタル用語辞典)する事ができます。

Important: キーにはユニークな、あまり他のライブラリと競合しそうにないオブジェクトを使うようにしてください。

例:デバッグログにzoneのローカルバリューを使う

foo.txtbar.txt の2つのファイルがあり、この2つの全ての行を print したい場合、プログラムは次のようになるでしょう:

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()の引数にファイル名を追加する事はできません。zoneローカルバリューを使うと、stringの返り値にファイル名を追加する事ができます。(追加した行をハイライトしています)

訳注: Qiitaではハイライトを表現できないため、コメントを追加しています。.map()runZoned()の部分の合計3行が追加されています。

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); }));
}

新しいコードが関数のシグネチャ(訳注:参考/メソッドのシグネチャ(signature)とメソッドの構文(syntax)の違い)を変更したり、splitLines()からsplitLinesStream()にファイル名を渡していない事に注意してください。かわりに、非同期のコンテキストで動作するstatic変数と同様の機能を実装するためにzoneのローカルバリューを利用しています。

関数のオーバーライド

runZoned()zoneSpecification引数を使う事で、zoneに管理されたかたちで関数がオーバーライドされます。引数の値でオーバーライドする事ができるZoneSpecificationのオブジェクトの機能は次の通りです:

  • 子のzoneをフォークする事ができる
  • zoneにコールバックを登録して実行できる
  • タイマーとmicrotaskをスケジューリングできる
  • 非同期のcatchされてないエラーを処理できる(onErrorはこのためのショートカットです)
  • printできます

例:printをオーバーライドする

関数をオーバーライドする簡単な例として、zoneのプリントを無視します:

import 'dart:async';

main() {
  runZoned(() {
    print('無視されるでしょう');
  }, zoneSpecification: new ZoneSpecification(
    print: (self, parent, zone, message) {
      // メッセージを無視します。
    }));
}

フォークされたzoneの中では、print()関数は単純にメッセージを破棄するprintにオーバーライドされています。(scheduleMicrotask()Timerコンストラクタのように)print()はその動作を現在のzone (Zone.current)で行っているため、printのオーバーライドが可能です。

委譲(Delegate)とインターセプタのための引数

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

この3つのインターセプタの引数は常に同じ順番で、他の引数の前にあります。

self
コールバックを処理するzoneです。
parent
親のzoneを表す `ZoneDelegate` です。親のzoneに対しての操作を転送するために使います。
zone
作業の元となったzoneです。いくつかの作業においてはどのzoneでその作業が発生したのか知る必要があります。例えば、`zone.fork`(の仕様)は、新しいzoneをzoneから作る必要があります。他の例では、`scheduleMicrotask()`を他のzoneに委譲する場合、元のzoneはmicrotaskを実行するひとつでなければなりません。

インターセプタが親にメソッドを委譲するとき、親(ZoneDelegate)バージョンのメソッドは、呼び出し元となった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())

例:親のzoneへの委譲

親のzoneにどのように委譲すればよいのか例を示します:

import 'dart:async';

main() {
  runZoned(() {
    var currentZone = Zone.current;
    scheduleMicrotask(() {
      print(identical(currentZone, Zone.current));  // 「true」が表示されます
    });
  }, zoneSpecification: new ZoneSpecification(
    scheduleMicrotask: (self, parent, zone, task) {
      print('zoneの内側でscheduleMicrotaskは呼ばれています');
      // タスクを実行する事ができるように、
      // 元の「zone」は、親に渡す必要があります。
      parent.scheduleMicrotask(zone, task);
    }));
}

訳注:上記の実行結果は以下のようになります。
zoneの内側でscheduleMicrotaskは呼ばれています
true

例:zoneに入る時と、zoneから出る時にコードを実行する

非同期のコードの実行時間を知りたい時があります。zoneにコードを置く事で、zoneに入る度にタイマーを起動し、zoneから出る度にタイマーを止める事で、これを行う事ができます。

ZoneSpecificationrun*パラメータを渡すする事で、zoneが実行するコードを指定する事ができます。

API note: 将来的にはzoneのコードを挟onEnter/onLeave APIという、一般的なケースにおいての代替手段をzoneが提供するかもしれません。詳細はissue 17532を見てください。

run*パラメータ(runrunUnaryrunBinary)は、zoneがコードの実行を要求されるたびに実行するコードを指定します。これらのパラメータは、それぞれ0個の引数、1つの引数、2つの引数のコールバックで動作します。runパラメータはrunZoned()が呼ばれた直後に実行され、初期の同期的なコードのために動作します。

run*を使ったプロファイリングの例です:

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();
  // ... 同期的に実行されるコード ...
  // ... 非同期で実行されるコード ...
    .then((...) {
      print(total.elapsedMilliseconds);
      print(user.elapsedMilliseconds);
    });
}, zoneSpecification: specification);

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

例:コールバックのハンドリング

zoneで非同期に実行されるコールバックのコードを、変更、あるいはラップするためにZoneSpecificationregister*Callbackパラメータを提供しています。run*パラメータのように、register*Callbackは、registerCallback(引数が無いコールバック)、registerUnaryCallback(1つの引数)、registerBinaryCallback(2つの引数)の3つの形式があります。

次のは、コードが非同期の中に消える前にスタックトレースを保存する例です:

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);
}

まずは例を実行してみてください。foo()が同期的に呼ばれてから、foo()に含まれる「最後のスタック」のスタックトレース(lastStackTrace)が見えると思います。次のスタックトレース(stackTrace)はfoo()ではなくbar()の非同期のコンテキストからのものです。

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

非同期のAPIを実装する場合も、すべてのzoneで対応する必要は無いかもしれません。例えば今のzoneを追跡し続けるために dart:ioライブラリを予想するかもしれませんが、代わりにFutureStreamのようなdart:asyncライブラリのクラスのzoneの処理に依存しています。

明示的にzoneを扱う場合、全ての非同期なコールバックを登録し、それぞれのコールバックが、登録されたzoneで呼び出される事を確認する必要があります。Zoneのヘルパーメソッドであるbind*Callbackは、これを簡単に提供します。これはregister*Callbackrun*のためのショートカットで、それぞれのコールバックがZoneに登録され実行される事を確認します。

bind*Callbackよりも細かいコントロールが必要な場合は、register*Callbackrun*を使うと良いでしょう。また、エラーが発生した場合の、try-catchとuncaughtErrorHandlerの呼び出しをラップする、Zonerun*Guardedメソッドを利用するとよいでしょう。

まとめ

zoneは確かに非同期のコードで発生する、catchされない例外からコードを保護するのに役立ちますが、それだけでなくもっとたくさんの事ができます。データをzoneに関連づけたり、printやタスクスケジュールなどのコアな機能をオーバーライドしたりできます。zoneはより良いデバッグを可能にし、プロファイリングなどのために利用できるフックを提供してくれます。

さらに詳しく

The Event Loop and Dart
FutureTimerscheduleMicrotask()を使ったタスクのスケジュールについて学びましょう(訳注:拙訳ですがこちらも翻訳しています)。
Zone-related API documentation
runZoned()ZoneZoneDelegateZoneSpecificationのドキュメントを読みましょう。
stack_trace
stack_traceライブラリのChain クラスによって、よりより非同期に実行されるコードのスタックトレースを得られるでしょう。詳細は、pub.dartlang.orgのstack_traceパッケージを見てください。

さらに例を見る

zoneを使ったさらに複雑な例があります。

The task_interceptor example
task_interceptor.darttoy zoneは、イベントループに屈せず、scheduleMicrotaskcreateTimercreatePeriodicTimerのDartのプリミティブな動作をシミュレートします。
stack_traceパッケージのソースコード
stack_traceパッケージは非同期のコードをデバッグするために、スタックトレースのチェーンを形成するzoneを使っています。エラーハンドリング、zoneローカルバリュー、コールバックといったzoneの機能を使っています。stack_traceのソースコードは、Dartプロジェクトのpkg/stack_trace/lib/srcで見る事ができます。
dart:html と dart:async のソースコード
これらの2つのライブラリは、非同期コールバックの主要なAPIを実装しており、そのため、zoneを使っています。DartプロジェクトのSourceタブから、ソースコードの閲覧やダウンロードが可能です。

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


翻訳ここまで --- 以下翻訳してみての感想


  1. イマイチよく分かってないとこあるので翻訳間違ってるかも(勘違いして間違った事書いてるかも)
  2. 気づいたら修正しますが、コメントでご指摘もらえると助かります
  3. 積極的に関数のオーバーライドとかする事は余りなさそうな気もしつつ、そういう機能とかzoneの引数とか知らないと、ライブラリのコード読む時になんじゃこりゃってなってハマりそうなのでしれてよかった。
  4. とりあえずは何も考えずにrunZoned()onErrorだけちゃんとつけるようにしとけばいいかな。
5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4