Wiele dobrych odpowiedzi tutaj, ale nadal chciałbym opublikować mój rant, ponieważ właśnie natknąłem się na ten sam problem i przeprowadziłem badania. Lub przejdź do wersji TLDR poniżej.
Problem
Oczekiwanie na taskzwrócone przez Task.WhenAlltylko zgłasza pierwszy wyjątek AggregateExceptionprzechowywanego w task.Exception, nawet jeśli wystąpił błąd wielu zadań.
Te obecne docs dlaTask.WhenAll słownie:
Jeśli którekolwiek z dostarczonych zadań zakończy się w stanie błędu, zwrócone zadanie również zakończy się w stanie Niepowodzenie, w którym jego wyjątki będą zawierać agregację zestawu nieopakowanych wyjątków z każdego z podanych zadań.
Co jest poprawne, ale nie mówi nic o wspomnianym wcześniej zachowaniu „rozpakowywania”, kiedy oczekuje się zwróconego zadania.
Przypuszczam, że doktorzy nie wspominają o tym, ponieważ to zachowanie nie jest specyficzne dlaTask.WhenAll .
Jest to po prostu Task.Exceptiontyp AggregateExceptioni dla awaitkontynuacji zawsze zostaje rozpakowane jako pierwszy wewnętrzny wyjątek, zgodnie z projektem. Jest to świetne w większości przypadków, ponieważ zwykle Task.Exceptionskłada się tylko z jednego wewnętrznego wyjątku. Ale rozważ ten kod:
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)
{
Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));
Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}
W tym przypadku wystąpienie polecenia AggregateExceptionzostaje rozpakowane do pierwszego wewnętrznego wyjątku InvalidOperationExceptionw dokładnie taki sam sposób, w jaki moglibyśmy go mieć Task.WhenAll. Moglibyśmy nie obserwować DivideByZeroException, gdybyśmy nie przeszli task.Exception.InnerExceptionsbezpośrednio.
Stephen Toub z Microsoftu wyjaśnia przyczynę tego zachowania w powiązanym problemie z GitHub :
Chodzi mi o to, że zostało to szczegółowo omówione lata temu, kiedy zostały one pierwotnie dodane. Pierwotnie zrobiliśmy to, co sugerujesz, z Task zwróconym z WhenAll zawierającym pojedynczy AggregateException, który zawierał wszystkie wyjątki, tj. Task.Exception zwrócił opakowanie AggregateException, które zawierało inny AggregateException, który następnie zawierał rzeczywiste wyjątki; wtedy, gdy był oczekiwany, zostanie propagowany wewnętrzny wyjątek AggregateException. Silna informacja zwrotna, którą otrzymaliśmy, która skłoniła nas do zmiany projektu, była taka, że a) zdecydowana większość takich przypadków miała dość jednorodne wyjątki, takie że propagowanie wszystkiego w sumie nie było tak ważne, b) propagowanie agregatu, a następnie przełamanie oczekiwań dotyczących połowów dla określonych typów wyjątków, oraz c) w przypadkach, w których ktoś chciał zagregować, mógł to zrobić wyraźnie z dwoma wierszami, tak jak napisałem. Przeprowadziliśmy również obszerne dyskusje na temat tego, jakie powinno być zachowanie await w odniesieniu do zadań zawierających wiele wyjątków i na tym wylądowaliśmy.
Jeszcze jedna ważna rzecz, na którą należy zwrócić uwagę, to zachowanie podczas rozpakowywania jest płytkie. Oznacza to, że tylko rozpakuje pierwszy wyjątek AggregateException.InnerExceptionsi pozostawi go tam, nawet jeśli jest to wystąpienie innego AggregateException. Może to dodać kolejną warstwę zamieszania. Na przykład zmieńmy w WhenAllWrongten sposób:
async Task WhenAllWrong()
{
await Task.FromException(new AggregateException(
new InvalidOperationException(),
new DivideByZeroException()));
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));
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));
}
Rozwiązanie (TLDR)
Wracając do await Task.WhenAll(...)tego, czego osobiście chciałem, to móc:
- Uzyskaj pojedynczy wyjątek, jeśli tylko jeden został zgłoszony;
- Uzyskaj,
AggregateExceptionjeśli więcej niż jeden wyjątek został zgłoszony łącznie przez jedno lub więcej zadań;
- Unikaj konieczności zapisywania
Taskjedynego w celu sprawdzenia jego Task.Exception;
- Propagować status anulowania prawidłowo (
Task.IsCanceled), a coś takiego nie zrobi, że: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.
Przygotowałem do tego następujące rozszerzenie:
public static class TaskExt
{
public static Task WithAggregatedExceptions(this Task @this)
{
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();
}
}
Teraz działa tak, jak chcę:
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");
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}");
}
AggregateException. Gdybyś użyłTask.Waitzamiastawaitw swoim przykładzie, złapałbyśAggregateException