もくじ
https://tera1707.com/entry/2022/02/06/144447#Task
やりたいこと
複数のTaskの完了を待つTask.WhenAll()
を使って、複数の処理を、複数のタスク(スレッド)で行わせた後、全部のタスクが終わるのを待つ、ということはよくやるのだが、今回、どれか一つが終わったら待ちを終わらせて先に進む、的なことをしたくなった。
具体的には、
Task.Run(()=> 本命処理)
と、Task.Delay(5000)
の5秒待ちを、Task.WhenAny()
で待たせて、- 基本本命処理の終了で抜けるけど、
- 処理を最長5秒で終わらせてタイムアウトさせたい
みたいなことをしたくなった。
したくなったのだが、そもそもWhenAnyをどう使っていいかよく知らない、 また、その時にTaskの中で例外が起きたりしたらどうしたらいいか?などがよくわからなかった。
そのときに、調べたことのメモ。
Task.WhenAny()の仕様
引数で受けたタスクの配列の、どれかが終わったら終了するタスクを返す。
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.whenany?view=net-8.0
戻り値は、引数で渡されたタスクのうち、完了したタスクが帰ってくる。
実験① 普通にタスク終了させる
実験コード
2秒、4秒で終わるタスクと、Task.Delay(6000)を、Task.WhenAny()で待つ。
using System.Diagnostics; namespace TaskWhenAnyJikken { internal class Program { static async Task Main(string[] args) { var t1 = Task.Run(() => { Thread.Sleep(2000); DebugWriteLine("t1 finish."); }); var t2 = Task.Run(() => { Thread.Sleep(4000); DebugWriteLine("t2 finish."); }); var wa = await Task.WhenAny(t1, t2, Task.Delay(6000)); DebugWriteLine($"t1 : {t1.Status} t2 : {t2.Status} t1==wa : {t1 == wa} t2==wa : {t2 == wa}"); Console.ReadLine(); } static void DebugWriteLine(string log) { Debug.WriteLine($"{DateTime.Now.ToString()} {log}"); } } }
結果
t1のタスクが、2秒でおわるので、2秒経過時点でawait Task.WhenAny(t1, t2, Task.Delay(6000));
を抜ける。
で、その時の戻り値wa
は、終わったタスクt1が入っている。
(なので、t1はwaと同じ参照先になる)
2023/12/18 22:33:15 t1 finish. 2023/12/18 22:33:15 t1 : RanToCompletion t2 : Running t1==wa : True t2==wa : False 2023/12/18 22:33:17 t2 finish.
実験② Task.Delay(5000)でタイムアウトさせる
実験コード
実験①のt1を6秒に、Delayを2秒に変えて、Delayの時間を一番短くしたもの。
using System.Diagnostics; namespace TaskWhenAnyJikken { internal class Program { static async Task Main(string[] args) { var t1 = Task.Run(() => { Thread.Sleep(6000); DebugWriteLine("t1 finish."); }); var t2 = Task.Run(() => { Thread.Sleep(4000); DebugWriteLine("t2 finish."); }); var wa = await Task.WhenAny(t1, t2, Task.Delay(2000)); DebugWriteLine($"t1 : {t1.Status} t2 : {t2.Status} t1==wa : {t1 == wa} t2==wa : {t2 == wa}"); Console.ReadLine(); } static void DebugWriteLine(string log) { Debug.WriteLine($"{DateTime.Now.ToString()} {log}"); } } }
結果
Task.Delayで抜けたので、他のタスクはまだ実行中。
終わったタスクはTask.Delay(6000)なので、Task.WhenAyn()の戻り値はt1,t2のどちらとも一致しない。
2023/12/18 22:40:48 t1 : Running t2 : Running t1==wa : False t2==wa : False 2023/12/18 22:40:50 t2 finish. 2023/12/18 22:40:52 t1 finish.
実験③ 例外がおきたとき
実験コード
起動後1秒で、タスク1番が例外を起こすようにした。
using System.Diagnostics; namespace TaskWhenAnyJikken { internal class Program { static async Task Main(string[] args) { DebugWriteLine("start."); var t1 = Task.Run(() => { Thread.Sleep(1000); throw new InvalidOperationException("reigai"); // 例外! //DebugWriteLine("t1 finish."); }); var t2 = Task.Run(() => { Thread.Sleep(4000); DebugWriteLine("t2 finish."); }); var wa = await Task.WhenAny(t1, t2, Task.Delay(6000)); DebugWriteLine($"t1 : {t1.Status} t2 : {t2.Status} t1==wa : {t1 == wa} t2==wa : {t2 == wa}"); DebugWriteLine("end."); Console.ReadLine(); } static void DebugWriteLine(string log) { Debug.WriteLine($"{DateTime.Now.ToString()} {log}"); } } }
結果
例外が起きて、タスク1が異常終了(Faulted)する。
異常終了したので、その時点で、Task.WhenAny()も終了する。
終了(異常終了)したのがt1なので、Task.WhenAny()の戻り値はt1である。
t1が異常終了するが、Task.WhenAny()が例外を上げはしない。
※これが、Task.WhenAll()
との、動き的な大きな違いか。
なので、t1が起こした例外を拾おうと思うと、以前調べたコチラでやったように、別途例外を拾わないといけない。
2023/12/18 22:45:34 start. 例外がスローされました: 'System.InvalidOperationException' (TaskWhenAnyJikken.dll の中) 2023/12/18 22:45:35 t1 : Faulted t2 : Running t1==wa : True t2==wa : False 2023/12/18 22:45:35 end. 2023/12/18 22:45:38 t2 finish.
実験④ WhenAnyに渡したTaskが例外を投げた場合にそれを拾う
実験③のt1が挙げた例外を拾えるようにしたコード。
t1が例外を上げていた場合、Task.WhenAny()が返してきた終了したタスクwa.Exception
(イコール、t1)のException
に、例外がAggregateException
として入っているので、さらにそいつのInnerException
プロパティを見る。
using System.Diagnostics; namespace TaskWhenAnyJikken { internal class Program { static async Task Main(string[] args) { DebugWriteLine("start."); var t1 = Task.Run(() => { Thread.Sleep(1000); throw new InvalidOperationException("reigai"); //DebugWriteLine("t1 finish."); }); var t2 = Task.Run(() => { Thread.Sleep(4000); DebugWriteLine("t2 finish."); }); var wa = await Task.WhenAny(t1, t2, Task.Delay(6000)); DebugWriteLine($"t1 : {t1.Status} t2 : {t2.Status} t1==wa : {t1 == wa} t2==wa : {t2 == wa}"); if (wa.Exception is AggregateException age) { DebugWriteLine(age.InnerException.GetType().ToString()); DebugWriteLine(age.InnerException.Message); } DebugWriteLine("end."); Console.ReadLine(); } static void DebugWriteLine(string log) { Debug.WriteLine($"{DateTime.Now.ToString()} {log}"); } } }
結果
例外を拾うときは、上記のようにする。
※WhenAllをWhenAnyに挿げ替えるような場合は、awaitしていても例外が上がってこないので、動きを合わせるなら、この方法で例外を受けて、再throwするなどの処置が必要そう。
2023/12/18 22:56:25 start. 例外がスローされました: 'System.InvalidOperationException' (TaskWhenAnyJikken.dll の中) 2023/12/18 22:56:26 t1 : Faulted t2 : Running t1==wa : True t2==wa : False 2023/12/18 22:56:26 System.InvalidOperationException 2023/12/18 22:56:26 reigai 2023/12/18 22:56:26 end. 2023/12/18 22:56:29 t2 finish.
参考
Task.WhenAny メソッド(msdocs)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.whenany?view=net-8.0
[C#] Taskの中で例外が起きた時のキャッチの仕方