2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Dart】initState内での非同期処理について 1. 実行順序 編

Posted at

はじめに

async/await を直接利用できない initState 内で非同期処理を行う際、外で定義している非同期処理メソッドを呼び出すことが多いかと思います。
私も、初めはそのように非同期処理を導入していたのですが、業務で先輩方が書かれたコードを見ると他にも下記のような導入方法がありました。

  • 無名関数 で導入するパターン
  • Future のコンストラクタを利用するパターン

これらに何か違いはあるのか?initState内では、どのように非同期処理を行うのが適切なのかが気になり調べてみました。

だいぶ長くなってしまったので、記事を分けています。
今回は、実行順序から見る違いについて触れている「実行順序編」になります。

  1. 実行順序編 ← 今回
  2. 応用編 (執筆予定)

initState について

initStateは、簡単にまとめると、

  • StatefulWidget のライフサイクルメソッドの1つで、Widgetの 状態 (State) の初期化処理を行う
  • この Widget (自身) がツリーへ挿入される時に呼び出される
  • フレームワークは、State オブジェクト初期化時に、このメソッドを1回だけ呼び出す (それ以降は、リビルドしても呼び出されない)

initStateは、StatefulWidgetにおいて重要な State オブジェクトのライフサイクルのスタート地点で、初期化処理の役目を担っています。
そのため、このメソッド内でデータの取得や初期化処理データの変更を監視するリスナーの設定 などといった操作をすることが適切とされています。

StatefulWidget のライフサイクルに関しては、下記の記事が参考になります。
ここではライフサイクルの詳細は省略します。

initState 内は、同期処理が前提

initStateは、前述の通り Widget がツリーに挿入されたばかりの状態 (state) を初期化している段階です。

そのため、ここで直接 async/await による非同期処理をさせてしまうと、処理が一時的にストップしてしまい UI の描画遅延やツリーが適切に構築されないといった問題を引き起こす恐れがあります。

実際、下記のように直接 async/await を使うと、

class _MyHomePageState extends State<MyHomePage> {

  late String _name;

  @override
  void initState() async {
    super.initState();

    //fetchDataは、非同期処理
    _name = await fetchData();
  }


  //以下略...

まず構文上問題ないので、コンパイルエラーにはなりませんが・・・

実行時に、恐怖の赤い画面が出てエラーになります。

内容は、

  • initState()がFutureを返している
  • initState()は、asyncキーワードを用いないvoidメソッドでないといけない
  • initState内で直接非同期処理を待機させるのでなく、待機せずにこの作業が実行できるように別のメソッドを呼ぶように

ということで、initState内で直接 async/await を使った非同期処理は実行できません。

initState内は、同期的に実行されることが前提となっています。

initState内での非同期処理

では、先ほどのエラー文でも推奨していた通り非同期処理を別のメソッドに持たせます。

  • 非同期処理メソッドを呼び出す
  late String name;

  @override
  void initState() {
    super.initState();

    fetchData();
  }

  Future<void> fetchData() async {
    name = await Future.delayed(
      const Duration(seconds: 2),
      () => "ポチ",
    );
    print('name: $name'); //name: ポチ
  }

こうすると実行時エラーも起きません。このように書くことが一般的かと思います。
ただ、他にも非同期処理を実現させる方法として例えば下記のようなものがあります。

  • 無名関数
  late String name;

  @override
  void initState() {
    super.initState();

    () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "ポチ",
      );
      print('name: $name'); //name: ポチ
    }();
  }

かなり極端な書き方ですが、これでも問題なく実行できます。
async を initStateではなく、無名関数に持たせる形です。

  • Futureのコンストラクタ
  late String name;

  @override
  void initState() {
    super.initState();

    Future(() async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "ポチ",
      );
      print('name: $name'); //name: ポチ
    });
  }

Futureクラスのコンストラクタに非同期の無名関数を渡す方法です。
Futureは、様々なコンストラクタがありますが、今回は FutureFuture.delayedにのみ言及します。

非同期処理の実行順序

では、これらの違いは何でしょうか?

いずれも2秒かけて name を取得するという処理をしており同じように見えますが、これらを並べて実行順を見た時に違いが出てきます。

実際に処理順を見ていきます。
Futureクラスのコンストラクタの1つとして、Futureだけでなく、Future.delayedコンストラクタ( Duration.zero ) も追加して試してみます。

実行順序確認①

まずは、上記の処理をinitState内で
非同期処理メソッド → 無名関数 → Futureコンストラクタ → Future.delayedコンストラクタの順に処理を書いてみます。

  late String name;

  @override
  void initState() {
    super.initState();

    //非同期メソッドの呼び出し
    fetchData();

    //無名関数
    () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "無名関数",
      );
      print('name: $name');
    }();

    //Futureコンストラクタ
    Future(() async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "Future",
      );
      print('name: $name');
    });

    //Future.delayedコンストラクタ
    Future.delayed(Duration.zero, () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "Future.delayed",
      );
      print('name: $name');
    });
  } 
  //initStateここまで
  

  //呼び出される非同期メソッド
  Future<void> fetchData() async {
    name = await Future.delayed(
      const Duration(seconds: 2),
      () => "非同期メソッド",
    );
    print('name: $name');
  }

ログ出力結果

name: 非同期メソッド
name: 無名関数
name: Future
name: Future.delayed

これは、誰もが予想する通りかと思います。
コードの順序通りに実行されています。

実行順序確認②

では、並び替えて
Futureコンストラクタ → Future.delayedコンストラクタ → 非同期処理メソッド → 無名関数
の順で処理を書いてみます。

  late String name;

  @override
  void initState() {
    super.initState();

    //Futureコンストラクタ
    Future(() async {
      name = await Future.delayed(
        const Duration(seconds: 2),
            () => "Future",
      );
      print('name: $name');
    });

    //Future.delayedコンストラクタ
    Future.delayed(Duration.zero, () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
            () => "Future.delayed",
      );
      print('name: $name');
    });

    //非同期メソッドの呼び出し
    fetchData();

    //無名関数
    () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "無名関数",
      );
      print('name: $name');
    }();

  }

  //呼びされる非同期メソッド
  Future<void> fetchData() async {
    name = await Future.delayed(
      const Duration(seconds: 2),
      () => "非同期メソッド",
    );
    print('name: $name');
  }

ログ出力結果

name: 非同期メソッド
name: 無名関数
name: Future
name: Future.delayed

Futureコンストラクタを先に呼び出しても、先程と同じ結果になりました。
Futureコンストラクタの処理は、後回しになっているのが分かります。

実行順序確認③

他の順序では、このようになりました。

コード上での処理順 (②から非同期メソッドと無名関数を並び替え)
Futureコンストラクタ → Future.delayedコンストラクタ → 無名関数 → 非同期メソッド

ログ出力結果

無名関数
非同期メソッド
Future
Future.delayed

無名関数と非同期メソッドには、実行順序の差はなさそうです。

実行順序確認④

コード上での処理順 (②からFutureとFuture.delayedを並び替え)
Future.delayedコンストラクタ → Futureコンストラクタ → 非同期処理メソッド → 無名関数
ログ出力結果

name: 非同期メソッド
name: 無名関数
name: Future.delayed
name: Future

FutureコンストラクタとFuture.delayedコンストラクタにも実行順序に差は無さそうです。

結果

このことから、実行の優先度が
無名関数及び非同期メソッド > Future及びFuture.delayedコンストラクタ
となっています。

では、これからFutureコンストラクタに渡すことで、何が起きているのかを見ていきます。

Dartにおける実行モデル

前提として、Dart では、シングルスレッド・イベントループ で処理を実行していきます。

全体像としては、下記画像のようになります。

スクリーンショット 2024-09-21 22.55.07.png
出典:The Event Loop and Dart

シングルスレッド

Dart は、Main isolate 上で処理が実行されています。

とりあえず、ここでは isolate とは、スレッドに似たようなものだと思ってもらえれば大丈夫です。
(スレッドとの違いは、後述します。)

イベントキューと呼ばれる待機列に格納されているイベントを、1つずつこの Main isolate 上で処理をしていきます。

この "1つずつ" というのがポイントで、シングルスレッドの特徴は並行処理です。

並行処理と並列処理

  • 並行処理
    処理を1つずつこなしている状態。
    このタスク1つ1つの切り替えを高速で行うことで、あたかも同時に処理しているかのように見せている。
    重いタスク処理が挟まってしまうと、後続のタスクは待機状態となってしまう。
  • 並列処理
    実際に、複数のタスクを同時に進めている状態。
    Dart では、 Main とは別の isolate を新たに用意することで実現可能。

isolate による並列処理も可能ですが、デフォルトではこの Main isolate のみ( シングルスレッド ) で並行処理で回しています。

isolateはスレッドとは厳密には違います。
isolateは、分離という意味の通り、

  • 各々独自のメモリ・ヒープやイベントループを持ち、isolate間でメモリを共有しない
  • メモリを共有しないので、データはメッセージによってやり取りを行う

というスレッドとは異なる特徴があります。
この記事では、isolate については深く触れていません。
詳細は、公式ドキュメント をご確認ください。

イベントループ

スクリーンショット 2024-09-22 17.51.09.png
出典:Concurrency in Dart

イベントループ とは、プログラム実行中に発生した様々なイベントを管理して、シングルスレッドでこれらを適切に処理していく為の仕組みです。
イベントループの役割は、イベントキューからイベントを取得→処理の操作を、キュー内のイベントが無くなるまで繰り返すことです。
シングルスレッドで、非同期処理を効率よく実行できているのも、このイベントループの仕組みによるものです。

(iOSで開発している方は、Run Loopといった方がしっくりくるかもしれないです。)

大まかに、こういったイベントがイベントループによって処理されます。

・ 非同期タスク

ネットワークリクエストやI/O処理など

・ タイマーイベント

Timer による遅延処理など

・ ユーザーによるアクション

タップやスクロール操作、キーボード入力など

・ UI描画処理

ウィジェットの更新など

イベントキュー と マイクロタスクキュー

イベントループは、2種類のキューからイベントを取得しています。
いずれも、FIFO (First In First Out) で処理を進めていきます。

① イベントキュー

基本的にイベントは、このキューに格納される。
例:I/O, UI描画処理, タイマー、非同期処理

Future および Future.delayed コンストラクタ は、このイベントキューにタスクをスケジューリングする。

② マイクロタスクキュー

次の方法で書かれた処理は、マイクロタスクキューに追加される。
イベントキューよりも優先的に実行させたい処理に使用。

例:非同期処理中のエラーハンドリングなど

scheduleMicroTask()

scheduleMicrotask (() { // マイクロタスクキューに追加したい処理 }); 

Future.microtask()

Future.microtask (() { // マイクロタスクキューに追加したい処理 }); 

・ その他 間接的な方法

  • すでに完了しているFuture で then() を呼び出す
  • Future.value()コンストラクター
  • Future.sync()コンストラクター (引数の関数がFutureを返さない場合)

これらのキューから、イベントループはどの順序でイベントを取り出しているのでしょうか。

イベントループの流れ

出典:Delayed code execution in Flutter

ここでは、initState() での流れを書いていきます。

1. 同期処理を順々に実行していく

(同期処理においては、イベントループは関与せず待機)

2. イベントループはマイクロタスクキューにタスクがあるか確認し、あればタスクを処理する

キュー内のマイクロタスクが全て実行し終えるまで繰り返す (FIFO 順)
マイクロタスクを全て実行し終えたら、3へ

3. イベントキューにあるタスクを (FIFO順で) 1つ実行 し、2 へ

この 2 ~ 3 のサイクルでイベントループは行われています。

このサイクルを見て分かる通り、マイクロタスクはイベントキュー内のイベントよりも優先的に処理されます。
マイクロタスク内の処理が全て完了しない限りイベントキュー内のイベントは処理されずに待機状態となるので、その名の通りマイクロタスクは軽量な小さいものに限定しなくてはなりません。
(むしろ、このキューはできる限り使用しないことが推奨されています)

initState 内で見る実行順序

Dartにおける実行モデルを理解したところで、initState 内での実行順序について記事前半で行った実行順序確認②を見返してみます。

コードは、確認時と同じものになりますので折りたたみます。
必要に応じて展開してください。

実行順序確認②コード
  late String name;

  @override
  void initState() {
    super.initState();

    //Futureコンストラクタ
    Future(() async {
      name = await Future.delayed(
        const Duration(seconds: 2),
            () => "Future",
      );
      print('name: $name');
    });

    //Future.delayedコンストラクタ
    Future.delayed(Duration.zero, () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
            () => "Future.delayed",
      );
      print('name: $name');
    });

    //非同期メソッドの呼び出し
    fetchData();

    //無名関数
    () async {
      name = await Future.delayed(
        const Duration(seconds: 2),
        () => "無名関数",
      );
      print('name: $name');
    }();

  }

  //呼びされる非同期メソッド
  Future<void> fetchData() async {
    name = await Future.delayed(
      const Duration(seconds: 2),
      () => "非同期メソッド",
    );
    print('name: $name');
  }

ログ出力結果

name: 非同期メソッド
name: 無名関数
name: Future
name: Future.delayed

前述のイベントサイクルの流れに沿っていきます。

-- 同期処理の実施 --

1. Futureの作成

  • コンストラクタの引数に渡されたタスクをイベントキューに追加 (イベントタスク1)

2. Future.delayedの作成

  • コンストラクタの引数で渡されたタスクをイベントキューに追加 (イベントタスク2)

イベントキューにタスクをスケジューリングしただけで、引数に渡されたタスクはまだ実行されていません。

3. fetchData() 呼び出し

  • fetchData() 内、await にて Future.delayedが作成され、2秒間の待機開始
    2秒間待機後、「"非同期メソッド" 文字列を返す」タスクをイベントキューに追加 (イベントタスク3)
    この待機中、4. 以降の処理を継続

4. 無名関数 呼び出し

  • 無名関数内、await にて Future.delayedが作成され、2秒間の待機開始
    2秒間待機した後、「"無名関数" 文字列を返す」タスクをイベントキューに追加 (イベントタスク4)
    この待機中、5. 以降の処理を継続

-- イベントループによる処理 --
マイクロタスクはないので、イベントキューから1つずつイベント処理をしていく

5. イベントタスク1 (Future.delayedの作成) を実行し、2秒間の待機開始
2秒間待機後、「"Future" 文字列を返す」タスクをイベントキューに追加 (イベントタスク5)
この待機中、6. 以降の処理を継続

6. イベントタスク2 (Future.delayedの作成) を実行し、2秒間の待機開始
2秒間待機後、「"Future.delayed" 文字列を返す」タスクをイベントキューに追加 (イベントタスク6)
この待機中、7. 以降の処理を継続

7. (3. 実行後2秒経過) イベントタスク3実行 ("非同期メソッド" 文字列を返す)
nameに代入され「非同期メソッド」が出力

8. (4. 実行後2秒経過) イベントタスク4実行 ("無名関数" 文字列を返す)
nameに代入され、「無名関数」が出力

9. (5. 実行後2秒経過) イベントタスク5実行 ("Future" 文字列を返す)
nameに代入され、「Future」が出力

10. (6. 実行後2秒経過) イベントタスク6実行 ("Future.delayed" 文字列を返す)
nameに代入され、「Future.delayed」が出力

このような順で実行されていることが分かります。
Futureコンストラクタ及びFuture.delayedコンストラクタは、タスクを一旦イベントループにスケジュールする関係で、非同期メソッドや無名関数より後に処理されています。

違いについて 結論

  • 非同期メソッド/無名関数
    内部で書かれた async/await による非同期処理は、メイン処理がコードに到達した時点で開始・待機

  • Future 及び Future.delayed コンストラクタ
    コンストラクタに渡した非同期処理は、イベントループに一旦スケジュールされるため、すぐには実行されない

この違いにより、処理タイミングに差が生じていました。
また、マイクロタスクを用いることで、さらに細かく処理のタイミングを調整できそうです。(ただ重い処理はNG)
イベントスケジュールのタイミングや、処理順序については学べましたが、どのようにこれらを管理して実行するのが適切かがまだ把握できていないので、その応用編ということで次回以降記事にまとめます。

参考文献

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?