原文: 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つです。
訳注:サンプルコード上の背景色に意味があり、Qiitaで表現できないので画像を貼り付けています。
青色が zone #1(ルートzone)、 緑色が zone #2 、 黄色が zone #3 を表しています。
次の図ではコードの実行順序に加えて、どのzoneでコードが実行されるのかを表しています。
runZoned()
を呼び出す度に、新しいzoneが生成され、コードはそのzoneで実行されます。(baz()
を呼び出すように)コードがタスクをスケジュールする時、そのタスクはスケジュールされたzoneで実行されます。例えば、(main()
の最後の行の) qux()
の呼び出しは、zone #2
で実行される future
に、渡されているにも関わらず、zone #1
(ルートzone) で実行されます。
子のzoneが親のzoneを置き換えるわけではありません。むしろ、新しいzoneは周りのzoneの中にネストされています。例えば zone #2
はzone #3
を含んでいますし、zone #1
(ルートzone)はzone #2
とzone #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.txt
と bar.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から出る度にタイマーを止める事で、これを行う事ができます。
ZoneSpecification
にrun*
パラメータを渡すする事で、zoneが実行するコードを指定する事ができます。
API note: 将来的にはzoneのコードを挟
onEnter
/onLeave
APIという、一般的なケースにおいての代替手段をzoneが提供するかもしれません。詳細はissue 17532を見てください。
run*
パラメータ(run
とrunUnary
とrunBinary
)は、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
で非同期に実行されるコールバックのコードを、変更、あるいはラップするためにZoneSpecification
のregister*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
ライブラリを予想するかもしれませんが、代わりにFuture
やStream
のようなdart:async
ライブラリのクラスのzoneの処理に依存しています。
明示的にzoneを扱う場合、全ての非同期なコールバックを登録し、それぞれのコールバックが、登録されたzoneで呼び出される事を確認する必要があります。Zone
のヘルパーメソッドであるbind*Callback
は、これを簡単に提供します。これはregister*Callback
とrun*
のためのショートカットで、それぞれのコールバックがZone
に登録され実行される事を確認します。
bind*Callback
よりも細かいコントロールが必要な場合は、register*Callback
とrun*
を使うと良いでしょう。また、エラーが発生した場合の、try-catchとuncaughtErrorHandler
の呼び出しをラップする、Zone
のrun*Guarded
メソッドを利用するとよいでしょう。
まとめ
zoneは確かに非同期のコードで発生する、catchされない例外からコードを保護するのに役立ちますが、それだけでなくもっとたくさんの事ができます。データをzoneに関連づけたり、printやタスクスケジュールなどのコアな機能をオーバーライドしたりできます。zoneはより良いデバッグを可能にし、プロファイリングなどのために利用できるフックを提供してくれます。
さらに詳しく
- The Event Loop and Dart
-
Future
やTimer
、scheduleMicrotask()
を使ったタスクのスケジュールについて学びましょう(訳注:拙訳ですがこちらも翻訳しています)。 - Zone-related API documentation
-
runZoned()
、Zone
、ZoneDelegate
、ZoneSpecification
のドキュメントを読みましょう。 - stack_trace
-
stack_trace
ライブラリのChain クラス
によって、よりより非同期に実行されるコードのスタックトレースを得られるでしょう。詳細は、pub.dartlang.orgのstack_traceパッケージ
を見てください。
さらに例を見る
zoneを使ったさらに複雑な例があります。
- The task_interceptor example
-
task_interceptor.dart
のtoy zone
は、イベントループに屈せず、scheduleMicrotask
やcreateTimer
、createPeriodicTimer
の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に感謝しています。
翻訳ここまで --- 以下翻訳してみての感想
- イマイチよく分かってないとこあるので翻訳間違ってるかも(勘違いして間違った事書いてるかも)
- 気づいたら修正しますが、コメントでご指摘もらえると助かります
- 積極的に関数のオーバーライドとかする事は余りなさそうな気もしつつ、そういう機能とかzoneの引数とか知らないと、ライブラリのコード読む時になんじゃこりゃってなってハマりそうなのでしれてよかった。
- とりあえずは何も考えずに
runZoned()
でonError
だけちゃんとつけるようにしとけばいいかな。