言語編 |
本ページは非同期処理に関するより詳細な解説であり、Asynchronous Programming: Futuresの翻訳をベースにしている。読者は非同期対応 (Asynchrony support)の章を読んだあとでこの章に進むことが好ましい。Dart 2の概要をまず知りたいユーザはこのページはスキップして構わない。
非同期処理のポイントは以下のようである:
Dartのコードは単一の実行「スレッド」のなかで走る
この実行スレッドを妨害するようなコードがあれば、そのプログラムは止まったまま(フリーズ状態)となる
Futureオブジェクトたち(futures)は非同期操作の結果を表現している – 処理或いはI/Oは後で完了する
あるfutureが完了するまで処理を保留するには、async関数のなかでawaitを使う(あるいはthen()を使う)
エラーを捕捉するには、async関数の中でtry-catch 式を使う(或いはcatchError()を使う)
並行処理でコードを走らせるにはアイソレートを作る(或いはウェブ・アプリの場合はworkerを使う)
Dartのコードは単一の実行「スレッド」のなかで走る。例えば長時間を要する計算処理を行うとかI/Oを待っているとかでDartコードが妨害されていると、プログラム全体が止まったまま(フリーズ状態)となる。非同期処理によりある操作の完了を待っている一方で他の作業を終わらせることが可能になる。Dartは非同期操作の結果を表現するのにFutureオブジェクト(futures)を使っている。 futuresを使ったコードを書くには、asyncとawaitを使うか、或いは Future APIを使うかの選択肢がある。
注意:総てのDartコードはそのDartコードが使うメモリの総てを所有する単一のアイソレートの環境の中で走る。Dartコードが走っていると、同じアイソレートの環境の中では他のコードは走れない。
複数のDartコードが並行して走らせたいという場合は、それらを別のアイソレートの中で走らせることが可能である(或いはウェブ・アプリの場合は代わりとしてworkerを使う)。複数のアイソレートは、通常CPUの各コアを使って、同時に並行して走る。アイソレートはメモリを共有しないので、相互間で関わりあう唯一の手段はメッセージを送信しあうことである。更なる詳細はisolateまたは web workersのAPIドキュメンテーションを参照のこと。またアイソレートに関してはアイソレート (Isolates)の章に記されている。
プログラムをフリーズさせ得るコードを見てみよう:
|
このプログラムは当日のニュースを集め、それをプリントし、次にそのユーザにとって関心があるその他のアイテムたちをプリントしている:
|
最初の行に当日のニュースnewsDigestがプリントされる。
このコードには問題がある:gatherNewsReports()は時間がかかり妨害するので、残りのコードはgatherNewsReports()があるファイルの中身を返した後でないと走らない。ファイル読み出しに時間がかかると、ユーザは宝くじに当たったか、明日の天気はどうか、今日の野球でどちらが勝ったかを知りたいなかで待たせられてしまう。
このアプリの応答性を維持できるようにするために、Dartのライブラリの著者たちは潜在的に貴重な仕事をこなす関数を定義する際に非同期モデルを使っている。そのような関数はfutureを使ってその値を返すようになっている。
futureはFuture<T>型のオブジェクトであり、型Tの結果を作る非同期操作を表現している。もしその結果が有効な値でないときは、そのfutureの型はFuture<void>である。futureを返すある関数が呼び出されると、2つのことが起きる:
その関数は完了を待つ仕事の待ち行列に置かれ、完了していないFutureオブジェクトを返す。
あとでその操作が終了したら、そのFutureオブジェクト値またはエラーでは完了する。
futureを使うコードを書くには2つの選択肢がある:
asyncとawaitを使う
FutureのAPIを使う
asyncとawaitはDart言語の非同期対応の要素であるキーワードである。これらを使うと一見同期コードのように見え、かつFutureのAPIを使わないで非同期コードが書ける。async関数はそのボディの前にasyncキーワードが置かれた関数である。awaitキーワードはasync関数の中でのみ機能する。
バージョンの注意: Dart 1.x版では、async関数は直ちに実行を保留する。Dart.2では即座に保留状態になるのではなく、最初のawaitまたはreturnに達するまでは同期状態で実行する。
以下のアプリは本サイトのファイルの中身を読み出すのにasyncとawaitを使って、ニュースを読み出すのをシミュレートしている。赤いボタンをクリックしてこのアプリを開始させるか、このアプリを含んだDartPadを開き、このアプリを開始させ、CONSOLEをクリックしてこのアプリの出力を見る。
printDailyNewsDigest()は最初に呼び出される関数であるにも拘らず、そのニュースは例え一行しか含んでいないファイルを介しているだけなのに最後にプリントされていることに注目されたい。これはこれを読みだしてプリントするコードが非同期で走っている為である。
この例では、printDailyNewsDigest()関数はgatherNewsReports()を非ブロックで呼び出している。gatherNewsReports()の呼び出しはなすべき仕事のの待ち行列に置かれるが、このコードの残りの実行を停止させない。このプログラムは宝くじの当たり番号、天気予報、及び野球の得点結果を印刷し;gatherNewsReports()がその作業を終了したらそれをプリントする。もしgatherNewsReports()がその仕事を終えるのに少しばかり時間がかかっても、大きな害とはならない:ユーザは毎日のニュース・ダイジェストがプリントされる前に他の事項が読める。
戻しの型に着目されたい。gatherNewsReports()の戻りの型はFuture<String>であり、これはstringの値で完了するfutureを返していることを意味する。printDailyNewsDigest()関数は値を返していないので、戻し値の型はFuture<void>である。
下図はこのコードを介した実行流れを示している。各番号は以下のステップに対応している。
|
このアプリの実行の開始
main()関数が非同期関数のprintDailyNewsDigest()を呼び、これにより非同期の実行が開始される
printDailyNewsDigest()はawaitを使ってgatherNewsReports()関数を呼び、実行を開始させる
gatherNewsReports()関数は未完了のfuture(Future<String>のインスタンス)を返す
printDailyNewsDigest()は非同期関数で、値を待っているので、その実行を待機し、未完了のfuture(この場合はFuture<void>のインスタンス)を呼び出しほう(main())に返す
残りの一連のprint関数が実行される。これらは同期関数なので、各関数はその仕事を完全に終了したら次のprint関数に移る。例えば、宝くじの当選番号は天気予報をプリントする前に全部プリントされる
main()は実行を終了した際に、非同期関数が実行を再開する。最初にgatherNewsReports()で返されたfutureが完了する。次にprintDailyNewsDigest()が実行を継続し、そのニュースをプリントする
printDailyNewsDigest()関数のボディが実行を終了したら、当初返されていたfutureが完了し、このアプリは終了する
async関数はすぐに(非同期で)実行を開始することに着目されたい。この関数は以下のいずれかに最初に出くわしたときに実行を保留し、未完了のfutureを返す:
この関数の中の最初のawait式(この関数がその式から未完了のfutureを取得した後で)
この関数の中の何らかのreturn文
この関数ボディの最後
Futureを返している関数がエラーで完了したときは、読者はそのエラーを捕捉したいと思うはずである。非同期関数は try-catchを使ってのエラー処理が可能である:
|
try-catchコードは同期コードで行うのと同じように、非同期コードでも同じやり方で振る舞う:もしtryブロック内のコードが例外をスローしたら、catch句内のコードが実行する。
複数のawait式を使って順番に各文が進行するように出来る:
|
expensiveB()関数はexpensiveA()が終了しないと実行を開始しない。以下同様。
Dart 1.9でasyncとawaitが導入される以前はFuture APIを使わざるを得なかった。現在でも古いコードの中だとか、async-awaitが提供していること以上の機能が必要な場合にFuture APIが使われているのを見かけるかもしれない。
Future APIを使って非同期のコードを書くには、コールバック関数を登録するのにthen()メソッドを使用する。このコールバックはFutureが完了したときに起動する。
以下のアプリは本サイトにあるファイルの中身を読み出すのに Future APIを使うことでニュースを読むという作業をシミュレートしている。このアプリは赤いボタンをクリックすると開始する。或いはこのアプリが入ったDartPadのウインドウを開いて、このアプリを開始させ、CONSOLEをクリックしてその出力を見ることができる。
rintDailyNewsDigest()は最初に呼び出される関数であるにも拘らず、そのニュースは例え一行しか含んでいないファイルを介しているだけなのに最後にプリントされていることに注目されたい。これはこれを読みだしてプリントするコードが非同期で走っている為である。
このアプリは以下のように進行する:
このアプリは実行を開始する
main関数がprintDailyNewsDigest()関数を呼び出し、これは即座に帰るのではなく、gatherNewsReports()を呼ぶ
gatherNewsReports()が開始しニュースを集め始めFutureを返す
printDailyNewsDigest()はthen()を使ってそのFuture への応答を指定している。then()を呼ぶことでthen()のコールバックが返す値で完了する新規のFutureを返している
残っているprint関数たちが実行される。例えば、宝くじの当選番号の総てがプリントされてから天気予報がプリントされる
ニュースの全部が到来したら、gatherNewsReports()が返したFutureは集めたニュースを含んだ文字列で完了する
printDailyNewsDigest()のなかのthen()で規定されたコードが走り、このニュースをプリントする
このアプリが終了する
注意:printDailyNewsDigest()関数の中において、future.then(print)というコードは以下と等価である:
|
別の書き方としては、then()の内部を波括弧を使って記述できる:
|
例えFutureがFuture<void>の型であってもthen()のコールバックには引数を用意してやらねばならない。慣例として未使用の引数は _ (アンダスコア)という名前を付す。
|
Future APIを使った場合はcatchError()を使ってエラーを捕捉できる:
|
newsのストリームの読み出しが出来ない場合は、このコードは以下のように動作する:
gatherNewsReports()が返したfutureはerrorで完了する
then()が返したfutureはerror; print() isn’t calledで完了する
catchError() (handleError())のコールバックがエラーを処理し、catchError()が返したfutureは通常完了し、このエラーは伝搬しない
then()にcatchError()をつなぐのはFuture APIを使った場合の一般的なパタンである。Future APIのこの連結はtry-catchブロックと等価なものだと考えればよい。
then()に似てcatchError()はそのコールバックが返した値で完了する新規のFutureを返す。
より詳細とサンプルは、Futures and Error Handlingに記されている。
Futureオブジェクトを返すexpensiveA(), expensiveB(), 及び expensiveC()の3個の関数を考えよう。これらの関数を順番に呼び出す(その前の関数が完了したらその関数がスタートする)、或いは3個総てを同時に開始させそのすべてが値を返したときに何かをすることができる。Futureインターフェイスは双方のケースに対応するに十分なメソッドを持っている。
Futureを返す関数たちを順番に走らせたいときは、then()呼び出しのチェインを使う:
|
ネストしたコールバックも動作するが、読みづらいので勧められない。
関数の実行順序が重要でない場合はFuture.wait()が使える。
Future.wait()にfutureたちのリストを渡すと、これは即座にFutureを返す。このfutureは与えられたfutureたちの総てが完了したら完了する。この時にはオリジナルのリストにある各futureが作った値たちが入ったリストで完了する。
|
もし呼び出した関数のどれかがエラーで終了したら、Future.wait()が返したFutureもまたerrorで完了する。そのエラーはcatchError()を使って処理する。
Dartにおけるfutureたちと非同期プログラミングに関するより詳細なドキュメンテーションを以下に挙げる:
The Event Loop and Dart:futureたちのマイクロタスクのスケジューリングの詳細
本資料の「非同期対応」の章
API参照:futures、isolates及びweb workers