这里有很多很好的答案,但是我仍然想发表我的观点,因为我遇到了同样的问题并进行了一些研究。或跳到下面的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
get解开为它的第一个内部异常InvalidOperationException
的方式与我们可能使用过的方式完全相同Task.WhenAll
。我们可能没有观察到DivideByZeroException
我们是否没有task.Exception.InnerExceptions
直接经历。
微软的斯蒂芬·图布(Stephen Toub)在相关的GitHub问题中解释了此行为背后的原因:
我要提出的观点是,几年前(最初添加这些内容时)进行了深入讨论。我们最初是按照您的建议执行的,从WhenAll返回的Task包含一个包含所有异常的AggregateException,即task.Exception将返回一个AggregateException包装器,该包装器包含另一个AggregateException,该包装器随后包含实际的异常;然后在等待它时,将传播内部AggregateException。我们收到的强烈反馈导致我们更改了设计,原因是:a)绝大多数此类案例都具有同质的例外情况,因此,将所有内容汇总传播并不那么重要,b)进行汇总传播,然后超出了对渔获量的期望对于特定的异常类型,c)对于有人想要聚合的情况,他们可以像我写的那样用两行明确地这样做。对于包含多个异常的任务,我们还进行了广泛的讨论,以了解等待的行为,这就是我们着手的地方。
要注意的另一件重要事情是,这种展开行为很浅。即,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(...)
,我个人想要的是能够:
- 如果只抛出一个异常,则获取一个异常;
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