Task.WhenAllで、複数タスクのうちどれか1つが終わるまで待つ & 例外処理

もくじ
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の中で例外が起きた時のキャッチの仕方

https://qiita.com/tera1707/items/d5a3bc12ffa5f80069a1