DartDay 14

The Event Loop and Dart (翻訳)

More than 3 years have passed since last update.

注意:英語よく分からないので間違ってたら教えてください


Dartとイベントループ


原文: https://www.dartlang.org/articles/event-loop/

2014/12/10時点の内容を元に翻訳


著者: Kathy Walrath

2013年9月著 (2013年10月更新)

非同期のコードはDartのいたるところで見受けられます。たくさんのライブラリのfunctionがFutureオブジェクトを返しますし、マウスのクリックや、ファイルのI/O完了、タイマーの完了などのイベントに応答するためのハンドラを登録する事ができます。

この記事ではDartのイベントループアーキテクチャを、あなたが驚く事もなく、より良い非同期なコードを書く事ができるように説明したいと思います。Futureタスクのスケジューリングの方法を学び、タスクの実行順序を予測できるようになるでしょう。


Note: この記事の中に出てくるものは、ネイティブに実行されるDartアプリ(Dart VMをdartコマンドやDartiumで使う)でも、JavaScriptにコンパイルされたDartアプリ(dart2jsで出力されたもの)のどちらにも適用できます。この記事では他の言語で書かれたソフトウェアと、Dartアプリを区別するために、(訳注:JSではなく)Dartを利用します。


この記事を読む前に、Future-based APIの基本的な使い方事を知っておくと良いでしょう。


基本的なコンセプト

もしあなたが、UIに関するコードを書いているのであれば、恐らく、イベントループとイベントキューのコンセプトは身近なものでしょう。それらはマウスクリックやグラフィック操作のイベントのように、一度に一つだけ処理される事に注目してください。


イベントループとキュー

イベントループの仕事は、イベントキューからひとつのアイテムを取り出し、それを処理するという、この2つのステップをキューがアイテムを持つ間繰り返す事です。

event-loop.png

キューに積まれたアイテムは、ユーザーの入力、ファイルI/O通知、タイマーなど、様々を表します。例えば、この図のイベントキューは、タイマーとユーザー入力を含んでいます:

event-loop-example.png

これは、あなたが知っているDart以外の言語によって、既に慣れている事かもしれません。さて、それではDartのプラットフォームにおいてどのように適用されているのかお話しましょう。


Dartのシングルスレッド実行

Dartがfunctionの実行を始めると、それが終了するまで実行を継続します。言い換えると、Dartのfunctionは他のDartのコードによって中断する事ができません。


Note: Dartのコマンドラインアプリは生成されたisolateによって並列に起動する事ができます。(DartのWebアプリは、現在は、追加でisolateを作成する事はできませんが、代わりにworkerを生成する事ができます。)isolateはメモリーを共有しません。isolateは、それぞれ別々のアプリのように、お互いにメッセージを送受信する事でコミュニケーションします。追加したisolateやworkerで実行される事を明示したコード以外の、アプリケーションのコードは、全てmainのisolate上で実行されます。さらに詳しい情報は、この記事で後述している 必要に応じて、isolateやworkerを利用してください の項目を参照してください。


次の図が示すように、Dartのアプリは、mainのisolateがアプリの main() function を実行した時に開始されます。main() が終わると、mainのisolateのスレッドがアプリのevent queueのアイテムをひとつずつ処理していきます。

event-loop-and-main.png

実際には、これはちょっと単純化しすぎていますが。


Dartのイベントループとキュー

Dartのアプリはひとつのイベントループと、2種類のキュー(event queuemicrotask queue)を持っています。

event queueは全ての外側のイベント(I/O, マウスイベント, 描画イベント, タイマー, Dartのisolate間のメッセージング 等)を含んでいます。

イベントハンドリングのコードは、時々、イベントループにコントロールを返す前にタスクを完了させる必要があるので、microtask queueが必要になります。例えば、オブジェクトの変更を監視している時に、いくつかの変化が同時に発生し、それを非同期で通知したい場合です。microtask queueは観測中のオブジェクトに、DOMが不整合な状態に見える前に、変更内容を通知する事が出来ます。

event queueはDartからのイベントと、システム上の他のイベントを含んでいます。現在、microtask queueはDartのコード上から生じたエントリーだけを含んでいます。しかし我々はWebの実装がブラウザのmicrotask queueに差し込む事を期待しています。(この件については, dartbug.com/13433 をご覧ください)

次の図が示すように、main()が終わったとき、イベントループは動き始めます。まず最初にすべてのmicrotaskをFIFO(訳注:First-In-First-Out)の順番で実行します。次に、イベントキューの先頭のアイテム(キュー)を取り出してして処理します。そしてこれを繰り返します。全てのmicrotaskを実行し、イベントキューにある次のアイテムを処理します。どちらのキューも空になって、これ以上のイベントを期待しなくなったとき、アプリを埋め込んだもの(例えばブラウザやテストフレームワーク)はそのアプリを処分する事ができます。


Note: Webアプリのユーザーがそのウィンドウを閉じた場合、イベントキューが空になる前でもそのWebアプリは終了する場合があります。


both-queues.png


Important: イベントループは、microtaskキューのタスクが実行されてる間、イベントキューを受け付けません。つまり、アプリはグラフィックの描画や、マウスのクリックの対応、I/Oへの反応などの事ができません。


あなたはタスクの実行順序を予測できますが、とはいえ、正確に、いつイベントループがキューからタスクを取り出すのかを予測する事はできません。Dartのイベントハンドリングシステムは、シングルスレッドのサイクルを基本としていて、tickや他の様々な時間測定には基づいてません。例えば、遅延タスクを作成した際に、あなたが指定した時間にそのイベントはキューに積まれます。ただし、そのイベントが処理される前に処理されるイベントキュー(と、microtask queuen内の全てのシングルタスク)がある間は、その遅延タスクは処理されません。


Tip: タスクの順序を指定するためのFutureの連鎖

もしあなたのコードが依存関係を持っている場合、それを明確にします。明確な依存関係は他の開発者がそのコードを理解しやすくし、そして、コードのリファクタリングにも耐久性があります。

ここに、間違ったコードの例があります:

// 設定と変数を利用する間の依存関係が明確でない悪いコード

future.then(...重要な変数をセット...);
Timer.run(() {...重要な変数を利用...});

かわりに、コードをこのようにします

// 依存関係が明確な良いコード

future.then(...重要な変数をセット...)
.then((_) {...重要な変数を利用...});

良いコードの方はthen()を利用し、変数を利用する前に必ずセットされている事を明確に示しています。(もし、エラーが発生した場合にもコードを実行したい場合は、then()の代わりにwhenComplete()を使う事ができます)

変数を利用するまでに時間がかかり、後で利用する場合は、コードをnew Futureに入れて考えてみます:

// 依存関係が明確で、かつ、遅延実行をしている、たぶん、より良いコード

execution.
future.then(...重要な変数をセット...)
.then((_) {new Future(() {...重要な変数を利用...})});

new Futureを使うと、イベントループにイベントキューから他のイベントを処理する機会を与える事ができます。次のセクションでは、コードを後から実行するスケジューリングの詳細を見ていきます。


タスクをどうやってスケジューリングするか

いくつかのコードを後で実行するようにする必要があるとき、dart:asyncライブラリで提供されている、次のAPIを利用する事ができます。



  1. Futureクラスは、 event queue の末尾にアイテムを登録します。

  2. トップレベル関数の scheduleMicrotask() は、 microtask queue の末尾にアイテムを登録します。


Note: scheduleMicrotask() 関数は runAsync() と呼ばれていました。(詳細はアナウンスを参照)


これらのAPIの利用例は、次のセクションの イベントキュー: new Future()microtaskキュー: scheduleMicrotask() にあります。


適切なキューを利用する(通常はイベントキュー)

可能な限り、Futureを使いイベントキューでタスクをスケジュールします。イベントキューを使う事でmicrotaskキューを短く保ちやすくなり、microtaskキューがイベントキューを枯渇させる可能性を減らします。

イベントキューからのアイテムが処理される前に、タスクを完全に完了させなければならない場合、基本的にはすぐにfunctionを実行しなければなりません。それができない場合、microtaskキューにアイテムを追加するために scheduleMicrotask() を利用します。例えば、Webアプリにおいて、js-interop(訳注:Github)プロクシの解放や、IndexedDBのトランザクションやイベントハンドラの終了を避けるためにmicrotaskを利用しています。

scheduling-tasks.png


イベントキュー: new Future()

イベントキューにタスクをスケジュールするためには、new Future()new Future.delayed() を使います。これらは dart:async ライブラリにおいて定義された Future クラスのコンストラクタのうちの2つです。


Note: また、タスクをスケジュールするために Timer を利用する事もできますが、もし、タスクの中でcatchしてない例外が発生した場合に、アプリケーションは終了してしまいます。代わりに、Timer に完了検知やエラー対応などの機能を追加した Future をお勧めします。


イベントキューにすぐにアイテムを置くために、new Future()を使います:

// イベントキューにタスクを追加します

new Future(() {
// ...何らかのコード...
});

新しい Future の処理が終わったあとすぐに何かコードを実行させたい場合、then()whenComplete()を追加する事ができます。例えば、次のコードは、新しい Future のタスクがキューから取り出された時に "42" を出力します:

new Future(() => 21)

.then((v) => v*2)
.then((v) => print(v));

new Future.delayed() を使って、いくらかの時間が経過したあとにアイテムをキューに積みます:

// 1秒後に、タスクがイベントキューに追加されます

new Future.delayed(const Duration(seconds:1), () {
// ...何らかのコード...
});

上記の例では1秒後にタスクがイベントキューに追加されますが、mainのisolateが待機(idle)状態になり、microtaskキューが空で、これより前にキューに積まれたイベントキューのエントリーがいなくなるまでそれは実行できません。例えば、main() function か、イベントハンドラが高負荷な計算処理を行っていた場合、タスクはその計算が終わるまで実行できません。この場合は、1秒よりはるかに遅れてタスクが実行される可能性があります。


Tip: Webアプリでアニメーションのフレームを描画する目的で Future(TimeやStreamも)を使わないでください。代わりに requestAnimationFrame のDartインターフェースの animationFrame を利用してください。


Future についての楽しい情報:


  1. Futureのthen()メソッドに渡したfunctionは、Futureが完了したあとすぐに実行されます。(functionはキューに積まれるのではなく、すぐに呼び出されます)


  2. then()が呼び出されるより前にFutureが完了した場合、microtaskキューにタスクが追加され、then()に渡されたfunctionが実行されます。


  3. Future()Future.delayed() コンストラクタはすぐには完了しません。これらはイベントキューにアイテムとして追加されます。


  4. Future.value() コンストラクタは #2 と同じように、microtask で完了します。


  5. Future.sync() コンストラクタは、(そのfunctionが Future を返さない限り)すぐに引数のfunctionを実行し、#2 と同じようにmicrotaskで完了します。


microtaskキュー: scheduleMicrotask()

dat:asyncライブラリはscheduleMicrotask()をトップレベルfunctionとして定義しています。scheduleMicrotask()は次のように利用する事ができます。

scheduleMicrotask(() {

// ...何らかのコード...
});

90019002のバグが原因で、最初に呼び出される scheduleMicrotask() はイベントキューにスケジュールされます。このタスクは scheduleMicrotask() に指定されたfunctionのmicrotaskキューを作成し、キューに積みます。microtaskキューが少なくとも1つのエントリーを持っている間は、次からの scheduleMicrotask() についてはmicrotaskキューに正しく追加します。microtaskキューが空になると、再び次回呼び出される scheduleMicrotask() を作成しなければなりません。

これらのバグの結論:最初に scheduleMicrotask() でスケジュールしたタスクは、イベントキューにあるように見えます。

この問題を回避するためには、最初の new Future() より前に scheduleMicrotask() を呼び出す事です。そうする事で、イベントキューにある他のタスクを実行するより前にmicrotaskキューを作成できます。しかしそれでも、イベントキューに追加される外部のイベントを止めることは出来ません。同様に、遅延タスクを持っている場合にも使えません。

microtaskキューにタスクを追加する他の方法としては、既に完了したFutureのthen()を呼び出す事です。詳細については、前のセクションを参照してください。


必要に応じて、isolateやworkerを利用してください

数値計算のタスクがある場合どうなるでしょうか?アプリケーションが応答可能であり続けるためには、独自のisolateかワーカーにタスクを持っている必要があります。isolateはDartの実装次第で、別のプロセスかスレッドで実行される場合があります。1.0ではWebアプリケーションがisolateやDartのワーカーをサポートする事を期待しないでください。しかし、JavaScriptのワーカーをDartのWebアプリに追加するための dart:html Workerクラス を使う事ができます。

いくつのisolateを使うべきでしょうか? 数値計算のタスクの場合、一般的にはいくつかのCPUが利用できる事を期待して、たくさんのisolateを使うべきです。

追加されたisolatesのいくつかは、純粋に計算処理を行ってる間、無駄になります。しかし、isolateが非同期に実行された(I/Oへの実行などなど)場合、CPUを浪費する事はなく、さらに多くのisolateをCPUの数よりも多く持つ事ができます。

CPUよりも多くのisolateを持つことができるというのは、あなたのアプリケーションにとって、とても良いアーキテクチャです。例えば、機能の各部分毎にisolateを分割したり、データが共有されてはいけない場合などです。


あなたの理解度をテストしてみましょう

さて、これであなたはタスクのスケジューリングに関しての全てを読み終えましたので、理解度をテストしてみましょう。

タスクの実行順序を指定するには、ダートのイベントキューの実装に依存してはならない事を覚えておいてください。実装は変わるかもしれないので Future の then()whenComplete() がより良い代替手段になります。もしもあなたがこれらの質問に正しく答える事ができたなら、きっとスマートに感じる事でしょう。


Question #1

このサンプルの出力はどうなっているでしょうか?

import 'dart:async';

main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 2'));

new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 3'));
new Future(() => print('future #3 of 3'));

scheduleMicrotask(() => print('microtask #2 of 2'));

print('main #2 of 2');
}

答え:

main #1 of 2

main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)

サンプルコードは3つのかたまりで実行されていて、あなたの期待どおりの順番でなければなりません:


  1. main() functionの中のコード

  2. microtaskキューの中のタスク(scheduleMicrotask())

  3. イベントキューの中のタスク(new Future()new Future.delayed())

最初から最後まで、main() function内のすべてのコールが同期的に実行する事を覚えておいてください。最初に main()print() が呼ばれ、次に scheduleMicrotask()、そして new Future.delayed() 、次に new Future()、といったように。コールバック(scheduleMicrotask()new Future.delayed()new Future()の引数に指定されたクロージャの中のコード)だけが後で実行されます。


Note: 現在、もしあなたが最初に呼ばれているscheduleMicrotask()をコメントアウトした場合、future #2 と #3 のコールバックの実行は microtask #2の前になります。microtaskキュー: scheduleMicrotask() で議論された 9001 と 9002 のバグが原因です。



Question #2

もっと複雑な例を用意しました。これの出力結果を正確に予測できるのであれば、あなたには花丸をあげます。

import 'dart:async';

main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 3'));

new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));

new Future(() => print('future #2 of 4'))
.then((_) => print('future #2a'))
.then((_) {
print('future #2b');
scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
})
.then((_) => print('future #2c'));

scheduleMicrotask(() => print('microtask #2 of 3'));

new Future(() => print('future #3 of 4'))
.then((_) => new Future(
() => print('future #3a (a new future)')))
.then((_) => print('future #3b'));

new Future(() => print('future #4 of 4'));
scheduleMicrotask(() => print('microtask #3 of 3'));
print('main #2 of 2');
}

9001と9002のバグがまだ修正されてなければ、以下のような出力になります。

main #1 of 2

main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
future #3 of 4
future #4 of 4
microtask #0 (from future #2b)
future #3a (a new future)
future #3b
future #1 (delayed)


Note: 9001/9002のバグが原因で、microtask #0 は future #4 の後に実行されます。本来は future #3 より前に実行されるべきです。このバグが現れるのは、future #2b の実行されるまでにmicrotaskにキューが積まれず、microtask #0 の結果がイベントキューの新しいタスクになり、新しいmicrotaskキューが作成されたためです。このmicrotaskキューは microtask #0 を含んでいます。もし microtask #1をコメントアウトしたなら、全てのmicrotaskは future #2c の後、 future #3 の前に丁度現れます。


前のと同じように、main() functionが実行され、次にmicrotaskキューのすべて、そしてイベントキューのタスクです。ここではいくつかの興味深いポイントを紹介します:


  • future #3 の then() のコールバックが new Future() を呼び出していて、新しいタスク(#3a)をイベントキューの最後に追加作成しています。

  • 全てのthen()のコールバックは、Futureの呼び出しが完了してすぐに実行されています。このため、future 2、2a、2b、2cは、埋め込み先(訳注:ブラウザやVM)にコントロールが戻る前に、全て一度に実行されています。同様に、future 3a と 3b も同時に実行されています。

  • もし、 3a のコードを then((_) => new Future(...)) から then((_) {new Future(...); }) に変えた場合、"future #3b" が前(future #3の後、future #3aの前)に表示されるでしょう。その理由は、コールバックからFutureを返すということは、コールバックで返されるFutureが完了したときに、then()によって返されたFutureが完了するように、これら二つのFutureを一緒にチェーンするための(それ自体が新しいFutureを返す)then()を取得する方法であるということです。詳しくは then()リファレンスを参照してください。


訳注: この部分(その理由は...以降)の翻訳が上手くできず、ほぼGoogle翻訳ままになってますが、内容としては以下のような感じです。

1. (_) => new Future() の場合は then() の返り値として新しいFutureが返ってる

2. (_) { new Future(); } の場合は、新しいFutureが生成されていますが、return句が無いため、thenへの返り値は何もない

この事から、1の場合(元の場合)は先に新たに生成したFuture(3a)が実行されるが、2の場合はイベントキューの末尾に生成したFuture(3a)が積まれるので、3bのほうが先に表示されるようになる感じです。



注釈付きのサンプルコードと出力

ここにQuestion #2の答えを明確にするかもしれないいくつかの図を用意しました。まず最初に、注釈付きのプログラムコードです。

test-annotated.png

そして、外部からのイベントが来ないていの場合に、いくつかのタイミングでのキューと出力がどのようになっているかの図です。

test-queue-output.png


まとめ

あなたはDartのイベントループと、どのようにタスクがスケジュールされるかを理解しておく必要があります。いくつかのDartのイベントループに関する主要な概念(コンセプト)を紹介します。


  • Dartのアプリは2つのキュー(イベントキューとmicrotaskキュー)からタスクを実行します。

  • イベントキューはDart(Future、Timer、isolate、メッセージなどなど・・・)とシステム(ユーザーアクション、I/Oなどなど・・・)両方のエントリーを持っています。

  • 現在のところ、microtaskキューはDartからのエントリーしかもちませんが、我々はそれがブラウザのmicrotaskキューとマージされる事を期待しています。

  • イベントループは次のイベントキューのアイテムを取り出して処理する前に、microtaskキューを空にします。

  • 両方のキューが空になったら、アプリの仕事は終わり、(その埋め込み先(訳注:ブラウザやVM)に応じて)終了する事ができます。

  • main() function、microtask キューやイベントキューの全てのアイテムは、Dartアプリのメインのisolateで実行されます。

タスクをスケジュールする場合は、いくつかのルールに従ってください:


  • 可能な限り、イベントキューを使ってください( new Future()new Future.delayed() を使って)

  • Futureの then()whenComplete() を使って、タスクの実行順序を指定してください。

  • イベントループの枯渇を避けるためには、可能な限りmicrotaskキューを短く保ってください。

  • アプリが応答可能な状態を保つために、イベントループ上での数値計算タスクを避けてください

  • 数値計算タスクを行う場合、isolateかworkerを追加で作成してください。

非同期コードを書く場合、これらのリソースが助けてくれる事でしょう



あとがき(ここから翻訳じゃないです)

以上が翻訳になります。


原文のライセンスについて

www.dartlang.org のソースコードはGitHubで公開されており、今回のThe Event Loop and DartGitHub上で管理されています

このレポジトリのLICENSEは、ドキュメント部分はCreative Commons Attribution 3.0 が適用されており、サンプルコードについてはBSDライセンスが適用されています。

詳細: https://github.com/dart-lang/www.dartlang.org/blob/master/LICENSE

CC3.0については利用時にライセンスを明記できていれば問題ないはずなので、特にContributorに連絡せずに翻訳&公開しています。

それで問題ないと思っていますが、もし何かまずい点などあればご指摘ください。


翻訳の動機

microtaskキューとかイベントキューとか言われてもよく分からなかったのでFuture触って勉強してたんですけど、このドキュメントが神すぎて辛かったので、アドベントカレンダーのネタにもなるしという事で翻訳しあした。英語むずい。辛い。