133

このコードでは:

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つによってスローされた1つの例外が返されます。

WhenAll常に作成するとは限りませんかAggregateException

4

8 に答える 8

86

これはすでに回答済みの質問ですが、選択した回答は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つのタスクだけが例外をスローした場合でも)。

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

于 2016-08-24T13:03:50.677 に答える
85

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

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

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

- 編集 -

とった:

BillWagnerによる非同期入門書

ビル・ワーグナーは次のように述べています

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

于 2012-08-17T14:40:40.847 に答える
44

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

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(); });
}
于 2016-01-29T07:18:07.630 に答える
30

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

問題

taskによって返されるのを待つと、複数のタスクに障害が発生した場合でも、に格納されTask.WhenAllているの最初の例外のみがスローされます。AggregateExceptiontask.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、最初の内部例外にラップ解除されます。直接通過しなければ、観察できなかったかもしれません。InvalidOperationExceptionTask.WhenAllDivideByZeroExceptiontask.Exception.InnerExceptions

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

私が言いたかったのは、何年も前にこれらが最初に追加されたときに、それが徹底的に議論されたということです。私たちは当初、あなたが提案していることを行いました。WhenAllから返されたタスクには、すべての例外を含む単一のAggregateExceptionが含まれています。つまり、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.IsCanceledこのようなものではそれができないため、キャンセルステータスを適切に伝達します( ) Task 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}");
}
于 2020-06-27T08:11:11.013 に答える
16

@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");
}
于 2018-04-02T00:02:33.290 に答える
11

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

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

于 2015-04-17T04:44:58.187 に答える
-3

これは私のために働く

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
于 2018-12-14T17:43:42.307 に答える
-4

コードでは、http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5で説明されているように、最初の例外が設計によって返され ます。 aspx

あなたの質問に関しては、次のようなコードを書くとAggreateExceptionが発生します。

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
于 2014-09-04T07:31:19.463 に答える