ここには多くの良い答えがありますが、同じ問題に遭遇し、いくつかの調査を行ったばかりなので、私はまだ私の暴言を投稿したいと思います。または、以下の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の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つの例外を取得します。
AggregateException
1つ以上のタスクによって複数の例外がまとめてスローされた場合に取得します。
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}");
}