合成:Race と WhenAll
複数のフローを組み合わせる方法を説明します。直列は await、「どれか 1 つ」は FlowTask.Race、「すべて」は FlowTask.WhenAll で書きます。タイムアウトや、ボタンによる読み込みや通信の中断も Race で書きます。
直列は await
Section titled “直列は await”async FlowTask Stage(){ await Intro(); await Battle(); await Outro();}順に待つだけなら、合成の API は要りません。
Race:最初に終わったものを取り、残りを止める
Section titled “Race:最初に終わったものを取り、残りを止める”FlowTask.Race は、枝のうち最初に終わったものを勝者にし、ほかの枝を取り消して巻き戻してから、呼び出し元を再開します。
async FlowTask<bool> Confirm(){ var r = await FlowTask.Race(okButton.Next(), cancelButton.Next(), FlowTask.WaitForSeconds(10)); return r.Index == 0; // OK pressed; Cancel or 10 s means no}- 2〜3 枝の Race は、枝ごとの型を持つ
RaceResult<T0, T1>(3 枝ならRaceResult<T0, T1, T2>)を返します。Indexは勝った枝の番号(0 始まり)、TryGet0(out v)などは勝ったときにその値を取り出し、Value0などは勝っていなければ例外を投げます。値を返さない枝(FlowTask)の値はFlowUnitです。 - 4 枝以上は配列などの
IReadOnlyListで渡します。FlowTaskのリストなら勝者の番号(FlowTask<int>)を、FlowTask<T>のリストならRaceResult<T>(IndexとValue)を返します。 - 各枝は合成の下の子スコープで走ります。
var r = await FlowTask.Race(hits.Next(), Patrol(self));if (r.TryGet0(out var hit)) await HitStun(self, hit);負けた枝は巻き戻ってから再開する
Section titled “負けた枝は巻き戻ってから再開する”負けた枝は、キャンセルと同じく巻き戻ります(CancelCause.RaceLost)。using と finally が走り、finally の await も最後まで走ります。呼び出し元が再開するのは、負けた枝の後始末がすべて終わってからです。
async FlowTask Patrol(Enemy self){ var effect = self.ShowAlert(); try { while (true) await self.WalkTo(NextPoint()); } finally { await effect.FadeOut(); // the caller of the Race resumes after this }}後始末を待たずに次へ進みたいなら、その後始末を FlowWorld.Current.Run(…) で別のフローとして始めます。後始末の await の規則は スコープとキャンセル にあります。
枝は書いた順に始まる
Section titled “枝は書いた順に始まる”枝は引数の順に、1 つずつ最初に止まる await まで走ります。始めた枝がその場で(同期に)終わったら、その枝が勝ち、残りの枝は始まりません。
同じフレームに複数の枝が終わったときは、処理された順で勝者が決まり、多くは書いた順になります(1 回の Tick で 2 つの期限が過ぎたときは、期限の早さではなく、その時間待ちを待ち始めた順です。枝が時間待ちそのものなら、書いた順になります。実行モデル)。そのため、割り込み(イベントの待ち)を先に書き、それで中断される処理を後に書きます。
// Good: a hit already buffered in the subscription wins before Patrol movesvar r = await FlowTask.Race(hits.Next(), Patrol(self));
// Worse: Patrol starts first and takes one step even if a hit is waitingvar r = await FlowTask.Race(Patrol(self), hits.Next());負けた枝が受け取っていた値
Section titled “負けた枝が受け取っていた値”購読(Subscription<T>)の Next() が Race に負けたとき、その待ちがすでに受け取っていた値は購読の先頭に戻ります。同じフレームに届いた被弾は、次の hits.Next() で取れます。失敗した WhenAll の枝の待ちも同じです。
- 戻した値は購読のあふれの方針に従います。満杯なら、
DropOldestとLatestは戻した値を捨て、DropNewestは最新の値を捨てます。Failは容量を超えて保持し、あふれとしては扱いません。 - エッジの
Signal.Next()が負けたときに受け取っていた値は消えます。取りこぼしたくない入力は購読で待ちます(シグナル)。 WithoutResult()で包んだNext()も、負けたときは同じく値が戻ります。- 戻るのは、合成が結果に取り込む前の値だけです。内側の Race が値を自分の結果に取り込んだ後に、その結果が外側の Race に負けて捨てられても、値は戻りません。
- ブリッジの値の扱い(
onDiscard)は Task / ValueTask にあります。
タイムアウト
Section titled “タイムアウト”タイムアウトの専用の API はありません。処理と WaitForSeconds を Race します。
var r = await FlowTask.Race(Download(), FlowTask.WaitForSeconds(10));if (!r.TryGet0(out var data)){ ShowTimeout(); // Download was unwound before this line return;}時間はスコープの Clock で進むので、ゲームのポーズ中はタイムアウトも止まります。止めたくないなら、UI の Clock や world.UnscaledClock を渡します:FlowTask.WaitForSeconds(10, world.UnscaledClock)(時間と Clock)。
どの Clock も Tick に渡した時間で進むので、実時間とは限りません(長いフレームは切り詰められ、アプリが止まっている間は進みません)。通信の上限のように実時間で計りたいものは、外部の側の仕組み(HttpClient.Timeout、CancellationTokenSource.CancelAfter、Unity の UnityWebRequest.timeout)で付けます。ゲームの時間で計るのは、ゲームの都合の上限(10 秒で答えなければ No など)です。
フローの終わりを上限付きで待つなら、Join も Race できます。
var r = await FlowTask.Race(handle.Join(), FlowTask.WaitForSeconds(5));読み込みや通信をボタンで止める
Section titled “読み込みや通信をボタンで止める”ブリッジした外部の処理も、Race の枝にできます。負けたブリッジのトークンは取り消されます。
var r = await FlowTask.Race( cancelButton.Next(), FlowBridge.FromTask(ct => File.ReadAllBytesAsync(path, ct)).ToFlowTask()); // the read stops when the button winsif (!r.TryGet1(out var bytes)) return;- 結果のあるブリッジは、
.ToFlowTask()を付けて渡します(付けないと型引数を推論できません)。 - トークンを取らない処理(Addressables の読み込みなど)は止まらずに最後まで走ります。届いた結果を解放するなら
onDiscardを渡します(Task / ValueTask、Unity のブリッジ)。 - 画面を閉じるなどで Race を含むフローが終わったときも、同じくトークンが取り消されます。
負けた枝を止めたくないとき
Section titled “負けた枝を止めたくないとき”Race の負けた枝は必ず止まります。止めずに「最初に終わったもの」だけを知りたいなら、処理を Flow.Spawn で始めて、Join を Race します。巻き戻るのは負けた Join の待ちだけで、処理そのものは続きます。
var a = Flow.Spawn(LoadA());var b = Flow.Spawn(LoadB());var first = await FlowTask.Race(a.Join(), b.Join()); // the other load goes onSpawn した処理は、呼んだスコープが終わると巻き戻ります。スコープより長く続けるなら FlowWorld.Run で始めます(フローと World)。
注意:1 つのハンドルを Join できるのは 1 回だけで、Race に負けた Join も 1 回に数えます。負けたほうの処理の終わりを後で待つなら、
FlowTask.WaitUntil(b, h => h.IsCompleted)で待ち、b.Resultを読みます。
WhenAll:すべてを待つ
Section titled “WhenAll:すべてを待つ”FlowTask.WhenAll は、すべての枝が終わるまで待ちます。
var (map, units) = await FlowTask.WhenAll(LoadMap(), LoadUnits());await FlowTask.WhenAll(FadeOutBgm(), FadeOutScreen()); // branches without values- 2〜3 枝は、値をタプルで返します(
Item1から始まる 1 始まり)。値を返さない枝の値はFlowUnitです。すべての枝が値を返さないなら、WhenAll も値を返しません。 - 4 枝以上は
IReadOnlyListで渡し、FlowTask<T>のリストなら値の配列(T[])を返します。
1 つの枝が例外で終わると、残りの枝を取り消して巻き戻し(CancelCause.Fault)、その後始末が終わってから await で例外を投げます。Task の WhenAll のように残りを走らせ続けたり、例外をまとめたりはしません。2 つ目以降の例外は Undelivered として報告されます(失敗の扱い)。
読み込みの失敗のような想定内の失敗で残りを止めるときも、失敗を専用の例外型で投げ、WhenAll を囲む catch でその型を受けます(失敗の扱い の「想定内の失敗」)。1 つが失敗しても残りを最後まで走らせ、枝ごとの成否を集めたいなら、各枝の中で例外を捕まえて、成否を値で返します。
値を捨てる:WithoutResult
Section titled “値を捨てる:WithoutResult”型の違う 4 つ以上の枝を FlowTask のリストにまとめたいときなど、値が要らない枝は WithoutResult() で値のない FlowTask にします。
var winner = await FlowTask.Race(new[]{ shopClicks.Next().WithoutResult(), // a Signal<FlowUnit> itemPicked.Next().WithoutResult(), // a Signal<Item>: its value is dropped closed.Next().WithoutResult(), FlowTask.WaitForSeconds(30),});if (winner == 0) await OpenShop();WithoutResult() で包んだ待ちが値を受け取ると(Race の枝なら、その枝が勝つと)、値はフローが受け取ったものとして扱われ、購読へは戻りません(ブリッジの値は onDiscard に渡ります。Task / ValueTask)。
規則と落とし穴
Section titled “規則と落とし穴”- 合成を作っただけでは、枝は始まりません。合成を await するか、
Flow.Spawn、FlowWorld.Runに渡したときに始まります。 - 1 つのタスクを渡せる合成は 1 つだけです。
- すでに始めたタスク(await したもの)を渡すと、合成を作る呼び出しが
FlowMisuseExceptionを投げます。 - まだ始めていないタスクを 2 つの合成に渡すと、作る時点では分かりません。後から始めた合成の await が
FlowMisuseExceptionを投げます。タスクは先に始めた合成のもので、その合成はそのまま続きます。 - 同じ合成に同じタスクを 2 回渡すと、その合成の await が
FlowMisuseExceptionを投げ、始めた枝は巻き戻されます。ただし、先の枝がすぐに終わると合成がそこで決着するので、誤用に気づかないまま進むことがあります。
- すでに始めたタスク(await したもの)を渡すと、合成を作る呼び出しが
Flow.NonCancelableで包んだタスクは合成に渡せません(FlowMisuseException)。- 枝の開始が失敗したら(使えない Clock の待ちなど)、合成はそれまでに始めた枝を巻き戻し、await で例外を投げます。
- 毎フレーム決着する Race(毎フレームの入力と処理の Race など)では、負ける枝を async メソッドではなく葉の待ち(
NextFrame、WaitForSeconds、Next、WaitUntil)にします。async メソッドを負けさせると、毎回巻き戻しの例外の費用がかかります(性能)。被弾や画面遷移のようにたまに起きる決着なら問題ありません。 WhenAnyとTimeoutに当たる API はありません。どちらも Race で書きます(タイムアウトは、処理とFlowTask.WaitForSecondsの Race です)。