Здесь много хороших ответов, но я все же хотел бы опубликовать свою тираду, поскольку я только что столкнулся с той же проблемой и провел некоторое исследование. Или перейдите к версии TL; DR ниже.
Эта проблема
Ожидание того, что task
возвращено, Task.WhenAll
вызывает только первое исключение из AggregateException
сохраненного в task.Exception
, даже если несколько задач дали сбой.
В настоящее время документы дляTask.WhenAll
говорят:
Если какая-либо из предоставленных задач завершается в состоянии сбоя, возвращенная задача также будет завершена в состоянии сбоя, где ее исключения будут содержать агрегирование набора развернутых исключений из каждой из предоставленных задач.
Это правильно, но ничего не говорится о вышеупомянутом поведении «разворачивания» при ожидании возвращенной задачи.
Я полагаю, что в документации об этом не упоминается, потому что такое поведение не является специфическим дляTask.WhenAll
.
Это просто Task.Exception
тип, AggregateException
и для await
продолжений он всегда по замыслу разворачивается как первое внутреннее исключение. Это отлично подходит для большинства случаев, поскольку обычно Task.Exception
состоит только из одного внутреннего исключения. Но рассмотрим этот код:
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 объясняет причину такого поведения в связанной проблеме GitHub :
Я пытался подчеркнуть, что это было подробно обсуждено много лет назад, когда они были изначально добавлены. Первоначально мы сделали то, что вы предлагаете, с помощью Task, возвращенного из WhenAll, содержащего одно исключение AggregateException, содержащее все исключения, т.е. task.Exception вернет оболочку AggregateException, которая содержит еще одно исключение AggregateException, которое затем будет содержать фактические исключения; затем, когда его ждут, будет распространено внутреннее исключение AggregateException. Полученная нами убедительная обратная связь заставила нас изменить дизайн: а) в подавляющем большинстве таких случаев были достаточно однородные исключения, так что распространение всего в совокупности не было так важно, б) распространение совокупности затем нарушило ожидания относительно уловов для определенных типов исключений, и c) для случаев, когда кому-то действительно нужна совокупность, они могут сделать это явно с помощью двух строк, как я написал. У нас также были обширные дискуссии о том, каким может быть поведение await в отношении задач, содержащих несколько исключений, и именно здесь мы остановились.
Еще одна важная вещь, на которую следует обратить внимание, это неглубокое поведение при разворачивании. То есть, он только развернет первое исключение из 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));
}
Решение (TL; DR)
Итак, вернемся к тому await Task.WhenAll(...)
, что я лично хотел, так это иметь возможность:
- Получить единственное исключение, если было выброшено только одно;
- Получить,
AggregateException
если одна или несколько задач коллективно сгенерировали более одного исключения;
- Избегайте необходимости сохранять
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}");
}
AggregateException
. Если бы вы использовалиTask.Wait
вместоawait
в своем примере, вы бы поймалиAggregateException