Здесь много хороших ответов, но я все же хотел бы опубликовать свою напыщенную речь, так как я только что столкнулся с той же проблемой и провел некоторое исследование. Или перейдите к версия TLDR ниже.
Проблема
Ожидание 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));
}
Решение (TLDR)
So, back to await Task.WhenAll(...)
, what I personally wanted is to be able to:
- Получить одно исключение, если было выброшено только одно;
- Получите
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}");
}
person
noseratio
schedule
27.06.2020
AggregateException
. Если бы вы использовалиTask.Wait
вместоawait
в своем примере, вы бы поймалиAggregateException
- person Peter Ritchie   schedule 17.08.2012Task.WhenAll
, и я попал в ту же ловушку. Поэтому я попытался вдаваться в подробности об этом поведении. - person noseratio   schedule 27.06.2020Task.Wait
блокирует, аawait
нет. - person xr280xr   schedule 27.03.2021