Task.WhenAllがAggregateExceptionをスローしないのはなぜですか?


100

このコードでは:

private async void button1_Click(object sender, EventArgs e) {
    try {
        await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2());
    }
    catch (Exception ex) {
        // Expect AggregateException, but got InvalidTimeZoneException
    }
}

Task DoLongThingAsyncEx1() {
    return Task.Run(() => { throw new InvalidTimeZoneException(); });
}

Task DoLongThingAsyncEx2() {
    return Task.Run(() => { throw new InvalidOperation();});
}

を待っていたタスクの少なくとも1つが例外WhenAllをスローしたため、を作成してスローすると予想しAggregateExceptionていました。代わりに、タスクの1つによってスローされた単一の例外が返されます。

WhenAll常に作成するわけではありませんAggregateExceptionか?


7
WhenAll はを作成ますAggregateException。あなたの例のTask.Wait代わりに使用した場合await、あなたはキャッチするでしょうAggregateException
ピーター・リッチー

2
+1、これは私が理解しようとしていることであり、デバッグとグーグルの時間を節約します。
kennyzx 2012年

数年ぶりにからのすべての例外が必要Task.WhenAllになり、同じ罠に陥りました。そこで、私はこの振る舞いについて詳細に調べてみました。
noseratio

回答:


75

私は正確にはどこにいるかは覚えていませんが、新しいasync / awaitキーワードでAggregateException実際の例外にラップされることをどこかで読みました。

したがって、catchブロックでは、集計された例外ではなく、実際の例外を取得します。これにより、より自然で直感的なコードを作成できます。

これは、既存のコードをasync / awaitを使用するように簡単に変換するためにも必要でした。この場合、多くのコードでは、集計された例外ではなく特定の例外を想定しています。

-編集-

とった:

ビル・ワグナーによる非同期プライマー

ビル・ワーグナー氏:(例外が発生した場合

... awaitを使用すると、コンパイラによって生成されたコードはAggregateExceptionをラップ解除し、基になる例外をスローします。awaitを活用することで、Task.Result、Task.Wait、およびTaskクラスで定義された他のWaitメソッドで使用されるAggregateExceptionタイプを処理するための余分な作業を回避できます。これが、基になるTaskメソッドの代わりにawaitを使用するもう1つの理由です。


3
ええ、私は例外処理にいくつかの変更があったことを知っていますが、Task.WhenAll状態の最新のドキュメント「提供されたタスクのいずれかがフォルト状態で完了すると、返されたタスクもフォルト状態で完了します。提供された各タスクからのラップされていない例外のセットの集約 "....私の場合、両方のタスクが障害状態で完了しています...
Michael Ray Lovett

4
@MichaelRayLovett:返されたタスクをどこにも保存していません。そのタスクのExceptionプロパティを見ると、AggregateExceptionが発生するでしょう。ただし、コードではawaitを使用しています。これにより、AggregateExceptionが実際の例外にラップされなくなります。
デサイクロン

3
私もそれを考えましたが、2つの問題が発生しました。1)タスクを保存する方法がわからないので、それを調べることができます(つまり、「タスクmyTask = await Task.WhenAll(...)」動作していないようです。2)awaitが複数の例外を1つの例外として表すことができるとは思いません。どの例外を報告する必要がありますか?ランダムに選択しますか?
Michael Ray Lovett、

2
はい、タスクを保存して待機のtry / catchで調べたところ、例外がAggregatedExceptionであることがわかりました。だから私が読んだドキュメントは正しい。Task.WhenAllが例外をAggregateExceptionにラップしています。しかし、それらを解くのが待っている。私は今、あなたの記事を読んでいるが、私はのawaitがAggregateExceptionsから単一の例外を選択し、別の1対1つを投げることができるか、まだ見ていない...
マイケル・レイ・ラヴェット

2
記事を読んで、ありがとう。しかし、awaitがAggregateException(複数の例外を表す)を単一の例外として表す理由がまだわかりません。例外の包括的な処理はどうですか?..例外をスローしたタスクと例外をスローしたタスクを正確に知りたい場合、Task.WhenAllによって作成されたTaskオブジェクトを調べる必要がありますか?
Michael Ray Lovett、

55

これは既に回答済みの質問であることはわかっていますが、選択した回答ではOPの問題は実際には解決されないので、これを投稿すると思いました。

このソリューションは、集計例外(つまり、さまざまなタスクによってスローされたすべての例外)を提供し、ブロックしません(ワークフローは依然として非同期です)。

async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception)
    {
        if (task.Exception != null)
        {
            throw task.Exception;
        }
    }
}

public async Task<int> A()
{
    await Task.Delay(100);
    throw new Exception("A");
}

public async Task<int> B()
{
    await Task.Delay(100);
    throw new Exception("B");
}

重要なのは、待機する前に集約タスクへの参照を保存することです。これにより、AggregateExceptionを保持するExceptionプロパティにアクセスできます(1つのタスクだけが例外をスローした場合でも)。

これがまだ役立つことを願っています。今日、私はこの問題を抱えていました。


これはIMOを選択する必要があります。
bytedev 2018

3
+1、しかしあなたは単にブロックのthrow task.Exception;中に入れることができないのですcatchか?(例外が実際に処理されているときに空のキャッチが表示されると混乱します。)
AnorZaken

@AnorZaken絶対に。もともとそのように書いた理由は覚えていませんが、欠点は見当たらないので、Catchブロックに移動しました。ありがとう
Richiban

このアプローチの小さな欠点の1つは、キャンセルステータス(Task.IsCanceled)が適切に伝達されないことです。これは、このような拡張ヘルパーを使用して解決できます。
noseratio

34

すべてのタスクをトラバースして、複数のタスクが例外をスローしたかどうかを確認できます。

private async Task Example()
{
    var tasks = new [] { DoLongThingAsyncEx1(), DoLongThingAsyncEx2() };

    try 
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex) 
    {
        var exceptions = tasks.Where(t => t.Exception != null)
                              .Select(t => t.Exception);
    }
}

private Task DoLongThingAsyncEx1()
{
    return Task.Run(() => { throw new InvalidTimeZoneException(); });
}

private Task DoLongThingAsyncEx2()
{
    return Task.Run(() => { throw new InvalidOperationException(); });
}

2
これは動作しません。WhenAll最初の例外で終了し、それを返します。参照:stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event

14
前の2つのコメントは正しくありません。コードは実際には機能し、exceptionsスローされた両方の例外が含まれています。
トビアス

DoLongThingAsyncEx2()は、新しいInvalidOperation()ではなく、新しいInvalidOperationException()をスローする必要があります
Artemious

8
ここでの疑問を軽減するために、この処理がどのように行われるかを正確に示す拡張フィドルをまとめました:dotnetfiddle.net/X2AOvMawait最初の例外がラップ解除される原因となっていることがわかりますが、実際にはすべての例外がタスクの配列を介して引き続き使用できます。
核ピジョン

13

@Richibanの回答を拡張して、タスクから参照することで、catchブロックでAggregateExceptionを処理することもできると言ったところです。例えば:

async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception ex)
    {
        // This doesn't fire until both tasks
        // are complete. I.e. so after 10 seconds
        // as per the second delay

        // The ex in this instance is the first
        // exception thrown, i.e. "A".
        var firstExceptionThrown = ex;

        // This aggregate contains both "A" and "B".
        var aggregateException = task.Exception;
    }
}

public async Task<int> A()
{
    await Task.Delay(100);
    throw new Exception("A");
}

public async Task<int> B()
{
    // Extra delay to make it clear that the await
    // waits for all tasks to complete, including
    // waiting for this exception.
    await Task.Delay(10000);
    throw new Exception("B");
}

11

あなたは考えているTask.WaitAll-それがスローされますAggregateException

WhenAllは、発生した例外のリストの最初の例外をスローするだけです。


3
これは間違っWhenAllていExceptionます。メソッドから返されたタスクには、でAggregateExceptionスローされたすべての例外を含むプロパティがありますInnerExceptions。ここで起こっていることawaitは、AggregateExceptionそれ自体ではなく最初の内部例外をスローすることです(デサイクロンが言ったように)。待機するWait代わりにタスクのメソッドを呼び出すと、元の例外がスローされます。
–ŞafakGür2018

2

ここでは多くの良い答えがありますが、同じ問題に遭遇していくつかの調査を行ったばかりなので、私はまだ怒りを投稿したいと思います。または、以下のTLDRバージョンにスキップしてください。

問題

複数のタスクが失敗した場合でも、taskによって返されるのを待つTask.WhenAllと、にAggregateException格納されている最初の例外のみがスローされtask.Exceptionます。

言うためTask.WhenAll現在のドキュメント

提供されたタスクのいずれかがフォルト状態で完了すると、返されたタスクもフォルト状態で完了します。その例外には、提供された各タスクからのラップされていない例外のセットの集約が例外に含まれます。

これは正しいですが、返されたタスクが待機するときの前述の「アンラップ」動作については何も述べていません。

私は、その動作はに固有ではないためTask.WhenAll、ドキュメントには記載されていないと思います。

それは単にTask.ExceptionタイプでAggregateExceptionあり、await継続のために、最初の内部例外として、設計により常にアンラップされます。通常Task.Exceptionは1つの内部例外のみで構成されるため、これはほとんどの場合に最適です。しかし、このコードを考えてみましょう:

Task WhenAllWrong()
{
    var tcs = new TaskCompletionSource<DBNull>();
    tcs.TrySetException(new Exception[]
    {
        new InvalidOperationException(),
        new DivideByZeroException()
    });
    return tcs.Task;
}

var task = WhenAllWrong();    
try
{
    await task;
}
catch (Exception exception)
{
    // task.Exception is an AggregateException with 2 inner exception 
    Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));

    // However, the exception that we caught here is 
    // the first exception from the above InnerExceptions list:
    Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
    Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}

ここでは、の場合とまったく同じ方法で、のインスタンスがAggregateException最初の内部例外InvalidOperationExceptionにラップ解除されますTask.WhenAll。直接DivideByZeroException通らなければ観察できなかっただろうtask.Exception.InnerExceptions

MicrosoftのStephen Toub、関連するGitHubの問題でこの動作の背後にある理由を説明しています

私が作ろうとしたのは、何年も前にこれらが最初に追加されたときに、それが詳細に議論されたことです。私たちは当初、すべての例外を含む単一のAggregateExceptionを含むWhenAllから返されたTaskで提案したことを行いました。つまり、task.Exceptionは、実際の例外を含む別のAggregateExceptionを含むAggregateExceptionラッパーを返します。その後、それが待機されると、内部のAggregateExceptionが伝搬されます。デザインを変更するきっかけとなった強いフィードバックは、a)そのようなケースの大部分がかなり均一な例外を抱えていたため、すべてを集約して伝播することはそれほど重要ではなかった、b)集約を伝播してから、漁獲に関する期待を裏切った特定の例外タイプについては、c)誰かが集計を必要としていた場合、私が書いたように、2行で明示的に集計を行うことができます。また、複数の例外を含むタスクに関して、awaitの動作がどうなるかについて、広範な議論がありました。

注意すべきもう1つの重要な点として、このアンラップ動作は浅いです。AggregateException.InnerExceptionsつまり、たとえ別ののインスタンスであったとしても、最初の例外のラップを解除してそこに残すだけAggregateExceptionです。これにより、さらに混乱の層が追加される可能性があります。たとえば、次のWhenAllWrongように変更してみましょう。

async Task WhenAllWrong()
{
    await Task.FromException(new AggregateException(
        new InvalidOperationException(),
        new DivideByZeroException()));
}

var task = WhenAllWrong();

try
{
    await task;
}
catch (Exception exception)
{
    // now, task.Exception is an AggregateException with 1 inner exception, 
    // which is itself an instance of AggregateException
    Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));

    // And now the exception that we caught here is that inner AggregateException, 
    // which is also the same object we have thrown from WhenAllWrong:
    var aggregate = exception as AggregateException;
    Assert.IsNotNull(aggregate);
    Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
    Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}

ソリューション(TLDR)

だから、に戻ってawait Task.WhenAll(...)、私が個人的に欲しかったのは、次のことができるようになることです。

  • 1つだけスローされた場合、1つの例外を取得します。
  • AggregateException1つ以上のタスクによって複数の例外がまとめてスローされた場合は、を取得します。
  • Taskをチェックするためだけを保存する必要はありませんTask.Exception
  • キャンセルステータスを適切に伝播します(Task.IsCanceledTask t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }

そのために、次の拡張機能をまとめました。

public static class TaskExt 
{
    /// <summary>
    /// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch
    /// </summary>
    public static Task WithAggregatedExceptions(this Task @this)
    {
        // using AggregateException.Flatten as a bonus
        return @this.ContinueWith(
            continuationFunction: anteTask =>
                anteTask.IsFaulted &&
                anteTask.Exception is AggregateException ex &&
                (ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ?
                Task.FromException(ex.Flatten()) : anteTask,
            cancellationToken: CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            scheduler: TaskScheduler.Default).Unwrap();
    }    
}

さて、以下は私が望む方法で動作します:

try
{
    await Task.WhenAll(
        Task.FromException(new InvalidOperationException()),
        Task.FromException(new DivideByZeroException()))
        .WithAggregatedExceptions();
}
catch (OperationCanceledException) 
{
    Trace.WriteLine("Canceled");
}
catch (AggregateException exception)
{
    Trace.WriteLine("2 or more exceptions");
    // Now the exception that we caught here is an AggregateException, 
    // with two inner exceptions:
    var aggregate = exception as AggregateException;
    Assert.IsNotNull(aggregate);
    Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
catch (Exception exception)
{
    Trace.WriteLine($"Just a single exception: ${exception.Message}");
}

1
素晴らしい答え
ロール

-3

これは私のために働く

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}

1
WhenAllと同じではありませんWhenAnyawait Task.WhenAny(tasks)タスクが完了するとすぐに完了します。したがって、すぐに完了して成功するタスクがあり、別のタスクが例外をスローするまでに数秒かかる場合、これはエラーなしですぐに戻ります。
StriplingWarrior

その後、ここでスローラインにヒットすることはありません。WhenAllが例外をスローした場合
thab

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.