RSS2.0

Dart 非同期処理の async / await について

Flutter でのアプリ開発にあたって Dart 言語を順序立って体系的に学ぶ余裕がなかったこともあり、必要になるポイント毎に言語仕様を調べていました。しかし、とうとう非同期処理で利用される await キーワードの動作について納得できない事象に遭遇し、きちんと調べ直すことにしました。今日は調べ直した Dart 言語の非同期処理について書いてみたいと思います。

事の発端: await をつけているのに処理が進んでしまう

Dart には await というキーワードがあり、非同期処理を行うメソッドの呼び出し時に追記することで、メソッド内部の非同期処理の完了を待つことができます。
以下のように書いた場合、sumNumber() メソッドの内容が非同期処理であっても、返り値を i で受け取るまで、その先に処理は進みません。
var i = await sumNumber();
これについては Dart の言語仕様を正しい理解したものですが、await をとりまく言語仕様について、もう少し考慮しなければならない点がありました。

例えば以下のようなコードを実行すると、どのように標準出力されるでしょうか。
import 'dart:async';

main() {
  ready();
  countDown();
  go();
}

void ready() {
  print("Ready set...");
}

void countNumber(number) {
  print(number);
}

void countDown() async {
  // ここで await を使って待機したい
  await countNumber(3);
  countNumber(2);
  countNumber(1);
}

void go() {
  print("Go!!");
}

await で処理が止まるので、
Ready set...
3
2
1
Go!!
となりそうですが、実際には以下のように出力されます。
Ready set...
Go!!
3
2
1
上記ソースコードからのこの出力結果が納得できない方が今回の対象読者です。私もこの記事を書くまで納得できませんでした。

Dart の非同期処理

Dart 言語における非同期処理についての公式ドキュメントEffective Dartを改めて読み直してみて、やはり順序だって学ぶのがよいと思ったので、そもそも非同期処理とはというところから説明していきたいと思います。
Dart 言語の非同期処理には主に 2 つの方法があります。1 つは Future クラスを利用すること、もう 1 つはメソッドに async をつけることです。

Future クラスに処理をわたす

Future クラスを利用すると、Future クラスに渡した処理を非同期で処理することができます。Future クラスに渡した処理は、処理が呼び出されつつも、呼び出し元では完了を待たずにその先に処理が進んでいきます。

例えば以下のコードでは、sumNumber() メソッドの内容は非同期で処理されます。
new Future(() => sumNumber());

Future クラスにはこれ以外の使い方がいくつもあって、以下のサンプルコードでは 1 秒間待機しています。
なお、1 秒間待機するのは非同期で処理されるタイマー処理であることに注意してください。Future クラスに渡された処理は、呼び出し元からすると非同期処理なので、タイマーが 1 秒間待機している間にも処理は先に進んでいきます。
import 'dart:async';

main() {
  ready();
  countDown();
  go();
}

void ready() {
  print("Ready set...");
}

void countNumber(number) {
  // Future クラスに渡された処理は、その完了を待たずに処理が進んでいく
  new Future.delayed(new Duration(seconds: 1));
  print(number);
}

void countDown() {
  countNumber(3);
  countNumber(2);
  countNumber(1);
}

void go() {
  print("Go!!");
}

このコードの実行結果は以下になります。
Ready set...
3
2
1
Go!!
実際に動かしてみるとわかりますが、Go!! と表示されたあとにプログラムが終了するまで 1 秒程度時間がかかっていて、Future クラスにより 1 秒待機されるのを待たずに、countNumber() の処理が進んでいくのがわかります。

countNumber() は計 3 回呼び出されていますが、実際にかかる時間が 1 秒程度なのも Future クラスが非同期処理されているからです。Future クラスに渡した処理が完了されるのを待たずに処理が進むため、実際にはタイマーが起動される時刻はほぼ同じになります。3 つのタイマーほぼ同時刻にスタートするので、1 秒しかまたなくてよいということになります。時間のかかる処理でも非同期処理ならば並列に実行できる(厳密には、Dart はシングルスレッドモデルですが)というのが、非同期処理のメリットになります。

async なメソッド

async がつけられたメソッドの内容は非同期で処理されます。async なメソッドは、メソッドが呼び出されつつも、呼び出し元ではメソッドの完了を待たずにその先に処理が進んでいきます。
async なメソッドは完了を待ってもらえませんが、処理されないわけではありません。Dart のスケジューラーがよきタイミングで処理してくれます。

以下のソースコードでは、(必要性は別として)countDown() に async をつけています。今度は 1 秒待つというようなことはせず、よりメソッドの実行順序にフォーカスした例となります。 
main() メソッド内では countDown() の呼び出しの後に go() が書かれていますが、countDown() は async なので、countDown() の完了を待たず、先に go() が処理されていきます。
import 'dart:async';

main() {
  ready();
  countDown();
  go();
}

void ready() {
  print("Ready set...");
}

void countNumber(number) {
  print(number);
}

// async をつけたメソッドは、メソッドの処理完了を待たずに、処理が先に進んでいく
void countDown() async {
  countNumber(3);
  countNumber(2);
  countNumber(1);
}

void go() {
  print("Go!!");
}
非同期処理というと並列実行されているようなイメージを持つと思います。並列に実行されるなら、どちらが先に実行されるのかは不定ではないかと思えますが、Dart 言語では async なメソッドは後回しにされるようです。
Dart のスケジューラーの動作については公式ドキュメントに記載がありますが、ここで重要なのはやはり async メソッドは完了まで待たれないという点です。 

このコードの実行結果は以下になります。
Ready set...
Go!!
3
2
1
async なcountDown() の前に go() が処理されているのがわかります。

そしてこれは、冒頭で事の発端としてあげたソースコードの実行結果と同じなのです。冒頭のソースコードでも countDown() には async がついていて、だからこのような順序で処理されていたのです。
ではメソッドに async をつけなければいいのではという話になりますが、冒頭の例でやりたいことを実現するにはもう少し知らなければならないことがあります。

Dart の非同期処理の終了を待つ

非同期処理を使っていると、その非同期処理の完了を待ちたいという場面が往々にしてあります。非同期でネットワークからデータを取得している間に別の処理を進めておき、そのあとデータ取得が終わるのを待って参照する、というようなケースです。

非同期処理は、デフォルトでは呼び出し元は処理の完了を待ちませんが、await キーワードをつけると完了を待つことができます。
await countDown();

await new Future.delayed(new Duration(seconds: 1));
await キーワードは async なメソッドの呼び出しに対しても、Future に対しても使うことができます。

以下のソースコードでは、countDown() の呼び出しと new Future.delayed() の呼び出しをそれぞれ await しています。
main() 内での countDown() 呼び出しを await したかったので、特に意味はありませんが、便宜上 countDown() は async にしました。
import 'dart:async';

// await をつかうメソッドは async でなければならない
main() async {
  ready();
  // メソッドの呼び出しに await をつけると、メソッドの完了をまって処理が進んでいく
  await countDown();
  go();
}

void ready() {
  print("Ready set...");
}

// await をつかうメソッドは async でなければならない
void countNumber(number) async {
  // Future の実行にも await をつけると、完了をまって処理が進んでいく
  await new Future.delayed(new Duration(seconds: 1));
  print(number);
}

void countDown() async {
  countNumber(3);
  countNumber(2);
  countNumber(1);
}

void go() {
  print("Go!!");
}
ここで、await を使っているメソッドは async でなければならないという Dart の言語仕様に注意してください。
内部で countDown() を await している main() メソッドは、async をつけなければ文法エラーになってしまいます。また同様に、Future クラスの呼び出しを await している countNumber() には async をつけなければなりません。

countDown() が async なメソッドになっていることはこの例ではほとんど意味がありませんが、countNumber() が async になったことはメソッドの実行順序に大きな影響があります。async な countNumber() 呼び出しが非同期処理されるため、数を数えるよりも先に go() が処理されます。
実行結果は以下のとおりです。
Ready set...
Go!!
3
2
1

await を使うメソッドは async でなければならないという Dart の言語仕様が、冒頭であげたソースコードの出力結果を理解するポイントとなります。

事の発端の真相

冒頭のソースコードでは、countDown() メソッド内で countNumber() メソッドの呼び出しに await をつけ、非同期処理を待とうとしていました。
しかし、内部でメソッドの呼び出しを await するためには、Dart の言語仕様上、countDown() 自身を async にする必要があります。
async なメソッドは非同期処理となるため、async な countDown() の呼び出しもとでは、countDown() の完了を待たずに処理が進んでいきます。

これが冒頭のソースコードがあのような出力結果となる理由でした。await キーワードに対する理解はあっていたものの、async キーワードに対する理解が足りていなかったのでした。
  FlutterDart  コメント (0)  2018/05/14 20:09:51


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2018, 11>>
28293031123
45678910
11121314151617
18192021222324
2526272829301