Dart-非同期の実行順序

Dart-Async/Awaitをちょっと調査にちょっと近い感じだけど、非同期対応(Future/マイクロタスク)とその実行順序についてしらべてみた。

非同期対応

Dartではasynchronyサポートと呼んでいて、これはFuture/Streamが該当する。

Deprecated tour of the Dart language
The previous destination of the Dart language tour.

上記にページに書かれてはいないのだけど、マイクロタスクもこの非同期対応の一部に含まれているのではないかと考えている。

マイクロタスクとはZone.scheduleMicrotaskで登録される関数。

scheduleMicrotask method - Zone class - dart:async library - Dart API
API docs for the scheduleMicrotask method from the Zone class, for the Dart programming language.

DartはFuture/Stream及びマイクロタスクをイベントループ的な処理で実行していて、awaitで登録されている処理の切り替えを行っている。

過去の記事になるけど、それに関しての実装概要みたいなものが記述されているのがインターネットアーカイブに残っている。

The Event Loop and Dart
Learn how Dart handles the event queue and microtask queue, so you can write better asynchronous code with fewer surpris...

切り替えに関しても優先順位が存在し、基本的にマイクロタスク、Future/Streamの順になっている。

非同期処理の実行順序

処理の実行順序を調べるため、とりあえず非同期対応を網羅しているプログラムを作ってみた。
元ネタは上のevent-loopページのQuestion #2のソース。

DartPad
An online Dart editor with support for console and Flutter apps.

これを実行すると処理の流れが見えてくる。

  1. awaitかmain関数から抜けるまで、基本呼び出し順序で処理される。
    0000000 main #1 of 2
    0000000 start
    0001000 func #0
    0001300 main #2 of 2
    • この部分で注目が「func #0」。
      これはfunc1()関数の中のprint文になるのだけど、この関数asyncでFutureを返す感じにしているのだけど、他の非同期処理に切り替わらず順序を守り処理されている。
  2. mainが抜けた時点でFuture及びマイクロタスク処理に移るのだけど、まず初めはマイクロタスクが実行される。
    0001400 microtask #1 of 3
    0001400 f.micro #1
    0001500 f.micro sync
    0002100 f.micro #1 end 1
    0002199 microtask #2 of 3
    0002199 microtask #3 of 3
    • microtask #2及び#3はfuture #2より後に登録されているのだけど、優先順位が高いのでfuture #2より前に処理されている。
    • f.microはFuture.microtaskで呼び出しており、一見Futureな感じだけど、中身はscheduleMicrotaskを使っているのでマイクロタスクと同じ優先順位になる。
    • f.micro syncはf.micro内で順序立てて処理されているのだけど、これはFuture.syncを使っているためで、Future.syncは指定された登録関数をまず呼び出す実装になっている。
      実行順序としてはfunc1()を呼び出したのと同じで、リターン値だけFutureでラッピングされている感じになる。
  3. mainに直書きされているFutureの実行
    0002699 future #2 of 4
    0002699 future #2a
    0002900 future #2b
    0002900 future #2c
    0002900 microtask #0 (from future #2b)
    0003000 func #0 future #1
    0003199 func #0 future #2
    0003199 future #3 of 4
    0003300 future #4 of 4
    • future #1はdelayedで遅延をかまされているので、今回の実行タイミングからは外されている。
    • func #0 future #1と#2はfunc1()内で登録されたFutureなのでmain側で登録されたFutureと同じタイミングでの実行になり、またその実行順序は登録順になっている。
    • microtask #0はfuture #2内で登録されたマイクロタスクなのでfuture #2が完了した時点でスケジュール、実行されている。
  4. Future内で登録されたFutureの実行
    0005199 future #3a (a new future)
    0005300 future #3b
    • future #3aはFuture #3内で新規に追加されたものなので、ひとつ前のタイミングより後に追加スケジュールされた感じになるので、ここで実行される。
  5. 遅延設定されたFutureの実行
    1002400 future #1 (delayed)

実行順序で気を付けなければいけないのは、Future.microtask、Future.syncになるのか。

Future.microtaskはFuture<T>を返したいのだけどscheduleMicrotaskの優先順位にしたいもの。
Future.syncは処理は即実行したいけどFuture<T>をリターンしたいという要求でできたものなのだろうか。

Future.syncは普通にFuture<T> func() {} みたいな関数を作って呼び出せばいいよう気がするのだけど。

Future.delayedの実際の遅延時間

サンプルプログラムでは、左に出てくる数値は、Stopwatchを使った経過マイクロ秒を表示している。

future #1のDurationの中身を変更することで、設定した遅延時間と実際の遅延時間の差を見て取ることができる。

今回の例では2.4msほどの差が出ている。

Durationでの遅延に関しては、シビアになると困る感じなのだろうか。

コメント

タイトルとURLをコピーしました