🔰

Dartの非同期処理(futures, async, await)をさわってみる

に公開

TL;DR

Dartの非同期処理について勉強したのでまとめました

はじめに

Dartでの非同期操作では、async, awaitなどのキーワードを使って操作します。
ですが、以下のような細かい挙動はどうなるのでしょうか?

・awaitで...
・awaitを使用するFuture関数がネストされている場合には?

実際に確認してみましょう。

環境

DartPad

基本的な使い方

Future<String> awaitFunc() {
  return Future.delayed(const Duration(seconds: 5), () => 'third');
}

void main() {
  print('first');
  awaitFunc().then((res) => {print(res)});
  print('second');
}

まずは上のようなasync, await, thenを使用したシンプルなコードを実行します。
実行結果としては以下のようになります。

first
second
third

処理順序としては、以下のようになります。APIリクエストの待ちなどでよくある処理だと思います。

①print('first')が実行され、出力される
②awaitFuncがコールされる
③print('second')が実行され、出力される
④awaitFuncの待ちが終了したので、出力される

同期的に実行させる

さて、次にFutureクラスの処理の結果を待ってから後続を実行してみましょう。

誤った例:awaitで待たない場合

Future<String> awaitFunc() {
  return Future.delayed(const Duration(seconds: 5), () => 'second');
}

void main() async {
  print('first');
  print(awaitFunc());
  print('third');
}

次に、Futureクラスが非同期処理なことをうっかり忘れてみましょう。上記のようなコードはどうなるのでしょうか?

first
Instance of '_Future<String>'
third

はい。期待していた出力ではないですね。

A future (lower case “f”) is an instance of the Future (capitalized “F”) class. A future represents the result of an asynchronous operation, and can have two states: uncompleted or completed.

https://dart.dev/codelabs/async-await

ドキュメントに記載されているように、Futureクラスは未完了と完了の2つの状態は持つことができます。そのため、特に待ちを行っていない上記処理ではFutureオブジェクトのまま出力されているようですね。

正しい例:awaitで待つ

Future<String> awaitFunc() {
  return Future.delayed(const Duration(seconds: 5), () => 'second');
}

void main() async {
  print('first');
  print(await awaitFunc());
  print('third');
}

Futureクラスの処理の実行を待つためには、awaitを使用します。また、awaitを使用する関数では、asyncを使用する必要があります。

first
second
third

Good!期待通りの結果です。

実行順序

次に、以下のような処理はどのような挙動となるでしょうか?

Future<String> awaitFunc1() {
  print('1-1');
  var res = Future.delayed(const Duration(seconds: 3), () => '1-2');
  print(res);
  print('1-3');
  return res;
}

Future<String> awaitFunc2() {
  print('2-1');
  var res = Future.delayed(const Duration(seconds: 3), () => '2-2');
  print(res);
  print('2-3');
  return res;
}

Future<String> awaitFunc3() {
  print('3-1');
  return Future.delayed(const Duration(seconds: 5), () => '3-2');
}

Future<String> awaitFunc4() async{
  print('4-1');
  await awaitFunc3().then((res) => {print(res)});
  print('4-2');
  return '4-3';
}

void main() async {
  print('first');
  awaitFunc1().then((res) => {print(res + 'fix')});
  print('second');
  await awaitFunc2();
  print('third');
  awaitFunc4().then((res) => {print(res + 'fix')});
  print('finish!');
}

実行結果としては以下のようになります。わかりづらいですね、出力順に意味がある処理などでは目にかかりたくないです。。

first
1-1
Instance of '_Future<String>'
1-3
second
2-1
Instance of '_Future<String>'
2-3
1-2fix
third
4-1
3-1
finish!
3-2
4-2
4-3fix

上記のポイントは以下です。
・awaitをつけて呼び出されたからといって、Futureクラスの内部処理すべてが同期的に処理されるわけではない(2-1->Instance of '_Future<String>' となっている)
・親処理の中で、呼び出された非同期処理の中で、awaitをつけて呼び出された孫の処理がある場合、親処理もawaitの実行待となる(third -> 4-1 -> 3-1 -> finish!となっている。親処理が孫のawaitに影響されない場合、thrird -> 4-1 -> finish -> ... の順となるはず)
・孫のawaitの待機終了後、処理順は親に戻る。(finish! -> 3-2 -> 4-2の順となっている)

まとめ

非同期処理は難しいですね。特に、親・子・孫関係以上で非同期処理と同期処理が混じると実行順序の保証が難しくなってきます。正しくハンドリングしたいですね。

参考

Discussion