如何从异步方法中返回AggregateException

时间:2019-04-15 10:58:01

标签: c# asynchronous aggregateexception

我有一个异步方法,其工作方式类似于增强型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();

一切正常,汇总中的汇总只是一个小麻烦。

3 个答案:

答案 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'scanton7'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
发生一个或多个错误。 (任务已取消。)(任务已取消。)(任务已取消。)