我有一个异步方法,其工作方式类似于增强型Task.WhenAll
。它需要执行大量任务,并在所有任务完成后返回。
public async Task MyWhenAll(Task[] tasks) {
...
await Something();
...
// all tasks are completed
if (someTasksFailed)
throw ??
}
我的问题是,当一个或多个任务失败时,如何获得一种类似于Task.WhenAll
返回的任务的方法?
如果我收集异常并抛出AggregateException
,它将包装在另一个AggregateException中。
编辑:完整示例
async Task Main() {
try {
Task.WhenAll(Throw(1), Throw(2)).Wait();
}
catch (Exception ex) {
ex.Dump();
}
try {
MyWhenAll(Throw(1), Throw(2)).Wait();
}
catch (Exception ex) {
ex.Dump();
}
}
public async Task MyWhenAll(Task t1, Task t2) {
await Task.Delay(TimeSpan.FromMilliseconds(100));
try {
await Task.WhenAll(t1, t2);
}
catch {
throw new AggregateException(new[] { t1.Exception, t2.Exception });
}
}
public async Task Throw(int id) {
await Task.Delay(TimeSpan.FromMilliseconds(100));
throw new InvalidOperationException("Inner" + id);
}
Task.WhenAll
的例外是AggregateException
,有2个内部例外。
对于MyWhenAll
,例外是AggregateException
,其中一个内部AggregateException
具有2个内部例外。
编辑:我为什么要这样做
我经常需要调用页面调度API:s,并希望限制同时连接的数量。
实际的方法签名是
public static async Task<TResult[]> AsParallelAsync<TResult>(this IEnumerable<Task<TResult>> source, int maxParallel)
public static async Task<TResult[]> AsParallelUntilAsync<TResult>(this IEnumerable<Task<TResult>> source, int maxParallel, Func<Task<TResult>, bool> predicate)
这意味着我可以像这样进行分页
var pagedRecords = await Enumerable.Range(1, int.MaxValue)
.Select(x => GetRecordsAsync(pageSize: 1000, pageNumber: x)
.AsParallelUntilAsync(maxParallel: 5, x => x.Result.Count < 1000);
var records = pagedRecords.SelectMany(x => x).ToList();
一切正常,汇总中的汇总只是一个小麻烦。
答案 0 :(得分:1)
async
方法仅针对每个集合最多针对返回的任务的单个异常,而不是多个。
这给您留下两个选择,您要么不能使用async
方法开始,而是要依靠其他方法来执行您的方法:
public Task MyWhenAll(Task t1, Task t2)
{
return Task.Delay(TimeSpan.FromMilliseconds(100))
.ContinueWith(_ => Task.WhenAll(t1, t2))
.Unwrap();
}
如果您有一个更复杂的方法,如果不使用await
则更难编写,那么您将需要解开嵌套的聚合异常,这样做很麻烦,尽管不是太复杂,但是: >
public static Task UnwrapAggregateException(this Task taskToUnwrap)
{
var tcs = new TaskCompletionSource<bool>();
taskToUnwrap.ContinueWith(task =>
{
if (task.IsCanceled)
tcs.SetCanceled();
else if (task.IsFaulted)
{
if (task.Exception is AggregateException aggregateException)
tcs.SetException(Flatten(aggregateException));
else
tcs.SetException(task.Exception);
}
else //successful
tcs.SetResult(true);
});
IEnumerable<Exception> Flatten(AggregateException exception)
{
var stack = new Stack<AggregateException>();
stack.Push(exception);
while (stack.Any())
{
var next = stack.Pop();
foreach (Exception inner in next.InnerExceptions)
{
if (inner is AggregateException innerAggregate)
stack.Push(innerAggregate);
else
yield return inner;
}
}
}
return tcs.Task;
}
答案 1 :(得分:0)
使用TaskCompletionSource
。
最外部的异常由.Wait()
或.Result
创建-记录为将存储在Task中的异常包装在AggregateException
内(以保留其堆栈跟踪-在之前引入ExceptionDispatchInfo
已创建)。
但是,Task实际上可以包含许多异常。在这种情况下,.Wait()
和.Result
将抛出包含多个AggregateException
的{{1}}。您可以通过TaskCompletionSource.SetException(IEnumerable<Exception> exceptions)
访问此功能。
因此,您不是想要创建自己的InnerExceptions
。在任务上设置多个例外,然后让AggregateException
和.Wait()
为您创建那个.Result
。
所以:
AggregateException
当然,如果您随后调用var tcs = new TaskCompletionSource<object>();
tcs.SetException(new[] { t1.Exception, t2.Exception });
return tcs.Task;
或await MyWhenAll(..)
,则它将仅引发第一个异常。这符合MyWhenAll(..).GetAwaiter().GetResult()
的行为。
这意味着您需要向上传递Task.WhenAll
作为方法的返回值,这意味着您的方法不能为tcs.Task
。您最终会做类似这样的丑陋的事情(调整问题中的示例代码):
async
不过,在这一点上,我将开始质疑为什么要尝试这样做,以及为什么不能直接使用从public static Task MyWhenAll(Task t1, Task t2)
{
var tcs = new TaskCompletionSource<object>();
var _ = Impl();
return tcs.Task;
async Task Impl()
{
await Task.Delay(10);
try
{
await Task.WhenAll(t1, t2);
tcs.SetResult(null);
}
catch
{
tcs.SetException(new[] { t1.Exception, t2.Exception });
}
}
}
返回的Task
。
答案 2 :(得分:0)
此答案结合了Servy's和canton7's解决方案中的想法。例如,使用了一个实际上有用的名为Retry
的方法,该方法在报告失败之前尝试多次运行任务。如果已达到最大尝试次数,则返回的任务将转换为故障状态,其中包含捆绑在AggregateException
中的所有异常。因此,此方法的行为与内置Task.WhenAll
方法非常相似。这是Retry
方法:
public static Task<TResult> Retry<TResult>(Func<Task<TResult>> taskFactory,
int maxAttempts)
{
if (taskFactory == null) throw new ArgumentNullException(nameof(taskFactory));
if (maxAttempts < 1) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
return FlattenTopAggregateException(Implementation());
async Task<TResult> Implementation()
{
var exceptions = new List<Exception>();
while (true)
{
var task = taskFactory();
try
{
return await task.ConfigureAwait(false);
}
catch (Exception ex)
{
exceptions.Add(ex);
if (exceptions.Count >= maxAttempts)
throw new AggregateException(exceptions);
}
}
}
}
该方法分为两个部分:1)参数验证和2)实现。该实现是异步的local function,它抛出AggregateException
。局部函数的结果任务表现不佳,因为失败时它包含嵌套的AggregateException
。为了解决此问题,在通过外部Retry
方法返回之前,将任务展平。它仅在顶层展平,这意味着可能更深的AggregateException
层次结构只会缩短一层。展平的目的仅仅是消除由AggregateException
方法抛出async
引起的顶级嵌套。这是展平方法:
private static Task<TResult> FlattenTopAggregateException<TResult>(Task<TResult> task)
{
var tcs = new TaskCompletionSource<TResult>();
HandleTaskCompletion();
return tcs.Task;
async void HandleTaskCompletion()
{
try
{
var result = await task.ConfigureAwait(false);
tcs.SetResult(result);
}
catch (OperationCanceledException ex) when (task.IsCanceled)
{
// Unfortunately the API SetCanceled(CancellationToken) is missing
if (!tcs.TrySetCanceled(ex.CancellationToken)) tcs.SetCanceled();
}
catch (Exception ex)
{
var taskException = task.Exception;
if (taskException == null || taskException.InnerExceptions.Count == 0)
{
// Handle abnormal case
tcs.SetException(ex);
}
else if (taskException.InnerExceptions.Count == 1
&& taskException.InnerException is AggregateException aex
&& aex.InnerExceptions.Count > 0)
{
// Fix nested AggregateException
tcs.SetException(aex.InnerExceptions);
}
else
{
// Keep it as is
tcs.SetException(taskException.InnerExceptions);
}
}
}
}
此方法包含一个async void
局部函数。这样做的原因是为了确保可以掩盖实施中的所有错误。如果不希望使用async void
方法,可以将其轻松转换为即发即弃任务。
这里是Retry
方法的用法示例。在try-catch块中等待返回的任务。被该块捕获的Exception
被忽略,而是观察任务的Exception
属性:
var task = Retry(async () =>
{
Console.WriteLine("Try to do something");
await Task.Delay(100, new CancellationToken(true));
return "OK";
}, maxAttempts: 3);
try
{
await task;
}
catch
{
if (task.IsFaulted)
{
Console.WriteLine($"Errors: {task.Exception.InnerExceptions.Count}");
Console.WriteLine($"{task.Exception.Message}");
}
else
{
Console.WriteLine("Not faulted");
}
}
输出:
尝试做某事
尝试做某事
尝试做某事
错误:3
发生一个或多个错误。 (任务已取消。)(任务已取消。)(任务已取消。)