LoginSignup
1
1

More than 3 years have passed since last update.

C#のawaitはAggregateExceptionを特別扱い(?)するので注意する + AggregateExceptionの扱い方メモ

Posted at

C#初心者です。

特別扱い、という言い方は正しいかはわかりませんが、私はそう読み取りました。詳しく調べてないので。

TL;DR

try-catchの中でawait Task.WhenAllをしたときにAggregateExceptionではなくTaskで発生したExcepotionが発生します。ちなみに、時間的に最初に起きたExceptionではなく、WhenAllの順番的に先頭に近いTaskで起きたExceptionが発生します。

どうしてもAggregateExceptionを得てちゃんと処理したい場合、以下の2つの方法があります。

  • awaitを使わずTask.WhenAll().Wait()とすると、AggregateExceptionを得られる。ただ、Wait()を呼んだスレッドは完全に止まる(はず)。ので、非同期処理的に良くない(はず)。
  • var tasks = Task.WhenAll()とした後、try-catch内でawaitする。その後、適当にExceptionでキャッチした後、tasks.Exceptionを参照してTaskがもつAggregateExceptionを得る(ドキュメントの例より

参考文献

PDFのここの文面から特別扱いしてないか?と読み取ってます

When you use await, the code generated by the compiler unwraps the AggregateException and throws the underlying exception.

発端

確認したときのコード(長いので省略)

--- Case4AwaitTaskWhenAll ---
Wandbox.FooException

--- Case5AwaitTaskWhenAll_Order ---
Wandbox.BarException

ここの動きがなんでや?ってなったので。

よくよく考えればTask.WhenAllはTaskを返すだけなので、Wait()すればAggregateExceptionが起きるのは分かるし、awaitすればawaitの都合によって処理されるんだろう。

でも直感的にはawaitでもAggregateExceptionが起きてほしくないですか?そうしないと2つ以上の例外が起きても1つしか処理できないので。

awaitと例外処理について触れている記述はあまり見かけなかったのですが、上記のPDFの最後の方でコンパイラがそのようなコードになるように書き換えているといった記述があります。
ただ、なぜこういう処理をしているのかはちょっと追い切れてないです。詳しい方教えてください。

以下AggregateExceptionのメモ

記事分けろって感じもしますが、近い内容なのでいいでしょう。

まず、AggregateExceptionについてのAPIドキュメントをみたが、なんのこっちゃ?という感じ。

こういう時は書いて覚えようということで、実際に動かしたのが以下のコード。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Wandbox
{
    class Program
    {
        static async Task throwError(string name, int millis)
        {
            await Task.Delay(millis);
            throw new NotImplementedException(name);
        }
        static void Main(string[] args)
        {
            try
            {
                Task.WhenAll(
                    throwError("1-1", 1000),
                    Task.Run(() => Task.WhenAll( // NEST
                        throwError("2-1", 1000),
                        Task.Run(() => Task.WhenAll( // NEST
                            throwError("3-1", 1000),
                            throwError("3-2", 1000),
                            throwError("3-3", 1000)
                        ).Wait()),
                        throwError("2-2", 1000)
                    ).Wait()),
                    throwError("1-2", 1000)
                ).Wait();
            }catch(AggregateException ex)
            {
                Func<Exception, string> ex2str = (x) => x.Message + ":" + x.GetType();
                Console.WriteLine("--- InnerExceptions ---");
                Console.WriteLine(string.Join("\n", ex.InnerExceptions.Select(ex2str)));
                Console.WriteLine("--- Flatten.InnerExceptions ---");
                Console.WriteLine(string.Join("\n", ex.Flatten().InnerExceptions.Select(ex2str)));

                try{
                    // 例外ハンドリングを行ったが、"1-1"しかハンドリングできなかったと仮定
                    ex.Handle(ex2 => ex2.Message == "1-1");
                }catch(AggregateException ex3){
                    Console.WriteLine("--- Unhandle InnerExceptions ---");
                    Console.WriteLine(string.Join("\n", ex3.InnerExceptions.Select(ex2str)));
                }
            }
        }
    }
}

出力

--- InnerExceptions ---
1-1:System.NotImplementedException
One or more errors occurred. (2-1) (One or more errors occurred. (3-1) (3-2) (3-3)) (2-2):System.AggregateException
1-2:System.NotImplementedException
--- Flatten.InnerExceptions ---
1-1:System.NotImplementedException
1-2:System.NotImplementedException
2-1:System.NotImplementedException
2-2:System.NotImplementedException
3-1:System.NotImplementedException
3-2:System.NotImplementedException
3-3:System.NotImplementedException
--- Unhandle InnerExceptions ---
One or more errors occurred. (2-1) (One or more errors occurred. (3-1) (3-2) (3-3)) (2-2):System.AggregateException
1-2:System.NotImplementedException

AggregateExceptionが持つメソッドやフィールドは邪魔者というわけではなく、ちゃんと例外処理を扱う上では使いこなす必要がある代物と感じます。

  • InnerExceptions: タスク実行完了時点で発生した例外をコレクションに詰めている。一応順序性はありそうだが、歯抜けにはならず詰められるため、何番目のタスクなのかは判別できないかも。
  • Handle(): InnerExceptionsの内容を1個づつ関数で処理する。関数には問題なく処理出来たらtrueを返し、ダメだったらfalseを返す関数を与える。もし処理できなかった例外が1つ以上あれば、Handle関数で処理されなかった例外が詰まったAggregateExceptionを発生させる。
  • Flatten(): AggregateExceptionの中にAggregateExceptionがあればそれを再帰的に展開する。

という感じです。

HandleかInnerExceptionsか、Flattenを使うか否か、という2点は考える必要はありそうです。

HandleかInnerExceptionsか、については、扱う例外の内容によるでしょう。
Flattenについては、AggregateExceptionを考慮したい(ネストしたタスクの例外を意識したい)か否かになるでしょう。ネストしたタスクの情報が無くなると困る場合は使わないで1個1個処理する形になるでしょう。細かいところだと、Flattenしたときに2-2が3-1より先に出ているので、ネストしているAggregateExceptionに対してFlattenを行うとネストを含んだ順序性は壊れそうです。

ちなみに、Handleの処理で例外を起こすと、AggregateExceptionに含まれるわけでもなく、そのままその例外が発生します。
当たり前という感じですが、その前までに処理できなかった例外(falseを返した例外)は失われ、以降の例外は処理されません。なので、Handleの処理で例外を起こさない方が良さそうです。

                try
                {
                    ex.Handle(ex2 => {
                        // 例外ハンドリングを行ったが、"1-1"しかハンドリングできなかった
                        if (ex2.Message == "1-1")
                        {
                            return true;
                        }
                        // "1-2"を別の例外でラップしてthrowした場合
                        if (ex2.Message == "1-2")
                        {
                            throw new Exception("Handle Exception", ex2);
                        }
                        return false;
                    });
                }
                catch (AggregateException ex3)
                {
                    // ここ呼ばれない
                    Console.WriteLine("--- Unhandle InnerExceptions ---");
                    Console.WriteLine(string.Join("\n", ex3.InnerExceptions.Select(ex2str)));
                }

wandboxには感謝しかない

1
1
1

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
1
1