简体   繁体   English

如何从异步方法返回 AggregateException

[英]How to return AggregateException from async method

I got an async method working like an enhanced Task.WhenAll .我得到了一个像增强的Task.WhenAll一样工作的异步方法。 It takes a bunch of tasks and returns when all are completed.它需要一堆任务,并在所有任务完成后返回。

public async Task MyWhenAll(Task[] tasks) {
    ...
    await Something();
    ...

    // all tasks are completed
    if (someTasksFailed)
        throw ??
}

My question is how do I get the method to return a Task looking like the one returned from Task.WhenAll when one or more tasks has failed?我的问题是,当一个或多个任务失败时,如何获得返回一个看起来像从Task.WhenAll返回的任务的方法?

If I collect the exceptions and throw an AggregateException it will be wrapped in another AggregateException.如果我收集异常并抛出AggregateException ,它将被包装在另一个 AggregateException 中。

Edit: Full Example编辑:完整示例

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);
}

For Task.WhenAll the exception is AggregateException with 2 inner exceptions.对于Task.WhenAll ,异常是具有 2 个内部异常的AggregateException

For MyWhenAll the exception is AggregateException with one inner AggregateException with 2 inner exceptions.对于MyWhenAll ,异常是AggregateException ,其中包含 1 个内部AggregateException和 2 个内部异常。

Edit: Why I am doing this编辑:为什么我这样做

I often need to call paging API:s and want to limit number of simultaneous connections.我经常需要调用分页 API:s 并想限制同时连接的数量。

The actual method signatures are实际的方法签名是

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)

It means I can do paging like this这意味着我可以像这样进行分页

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();

It all works fine, the aggregate within aggregate is just a minor inconvenience.一切正常,聚合中的聚合只是一个小小的不便。

async methods are designed to only every set at most a single exception on the returned task, not multiple. async方法被设计为仅在返回的任务中设置每个最多一个异常,而不是多个异常。

This leaves you with two options, you can either not use an async method to start with, instead relying on other means of performing your method:这给您留下了两个选择,您可以不使用async方法开始,而是依赖其他方法来执行您的方法:

public Task MyWhenAll(Task t1, Task t2)
{
    return Task.Delay(TimeSpan.FromMilliseconds(100))
        .ContinueWith(_ => Task.WhenAll(t1, t2))
        .Unwrap();
}

If you have a more complex method that would be harder to write without using await , then you'll need to unwrap the nested aggregate exceptions, which is tedious, although not overly complex, to do:如果你有一个更复杂的方法,如果不使用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;
    }

Use a TaskCompletionSource .使用TaskCompletionSource

The outermost exception is created by .Wait() or .Result - this is documented as wrapping the exception stored inside the Task inside an AggregateException (to preserve its stack trace - this was introduced before ExceptionDispatchInfo was created).最外层的异常由.Wait().Result创建 - 这被记录为将存储在 Task 中的异常包装在AggregateException中(以保留其堆栈跟踪 - 这是在创建ExceptionDispatchInfo之前引入的)。

However, Task can actually contain many exceptions.但是,Task 实际上可以包含许多异常。 When this is the case, .Wait() and .Result will throw an AggregateException which contains multiple InnerExceptions .在这种情况下, .Wait().Result将抛出一个包含多个InnerExceptionsAggregateException You can access this functionality through TaskCompletionSource.SetException(IEnumerable<Exception> exceptions) .您可以通过TaskCompletionSource.SetException(IEnumerable<Exception> exceptions)访问此功能。

So you do not want to create your own AggregateException .所以您不想创建自己的AggregateException Set multiple exceptions on the Task, and let .Wait() and .Result create that AggregateException for you.在任务上设置多个异常,并让.Wait().Result为您创建AggregateException

So:所以:

var tcs = new TaskCompletionSource<object>();
tcs.SetException(new[] { t1.Exception, t2.Exception });
return tcs.Task;

Of course, if you then call await MyWhenAll(..) or MyWhenAll(..).GetAwaiter().GetResult() , then it will only throw the first exception.当然,如果您随后调用await MyWhenAll(..)MyWhenAll(..).GetAwaiter().GetResult() ,那么它只会抛出第一个异常。 This matches the behaviour of Task.WhenAll .这与Task.WhenAll的行为相匹配。

This means you need to pass tcs.Task up as your method's return value, which means your method can't be async .这意味着您需要将tcs.Task作为方法的返回值传递,这意味着您的方法不能是async You end up doing ugly things like this (adjusting the sample code from your question):您最终会做这样丑陋的事情(根据您的问题调整示例代码):

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 });
        }
    }
}

At this point, though, I'd start to query why you're trying to do this, and why you can't use the Task returned from Task.WhenAll directly.不过,在这一点上,我会开始询问您为什么要尝试这样做,以及为什么您不能直接使用从Task.WhenAll返回的Task

I got an async method working like an enhanced Task.WhenAll .我得到了一个像增强的Task.WhenAll一样工作的异步方法。 It takes a bunch of tasks and returns when all are completed.它需要一堆任务,并在所有任务完成后返回。

public async Task MyWhenAll(Task[] tasks) {
    ...
    await Something();
    ...

    // all tasks are completed
    if (someTasksFailed)
        throw ??
}

My question is how do I get the method to return a Task looking like the one returned from Task.WhenAll when one or more tasks has failed?我的问题是,当一个或多个任务失败时,如何获得返回Task.WhenAll返回的 Task 的方法?

If I collect the exceptions and throw an AggregateException it will be wrapped in another AggregateException.如果我收集异常并抛出一个AggregateException ,它将被包装在另一个 AggregateException 中。

Edit: Full Example编辑:完整示例

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);
}

For Task.WhenAll the exception is AggregateException with 2 inner exceptions.对于Task.WhenAll ,异常是AggregateException有 2 个内部异常。

For MyWhenAll the exception is AggregateException with one inner AggregateException with 2 inner exceptions.对于MyWhenAll ,异常是AggregateException带有 1 个内部AggregateException和 2 个内部异常。

Edit: Why I am doing this编辑:为什么我这样做

I often need to call paging API:s and want to limit number of simultaneous connections.我经常需要调用分页 API:s 并希望限制同时连接的数量。

The actual method signatures are实际的方法签名是

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)

It means I can do paging like this这意味着我可以像这样进行分页

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();

It all works fine, the aggregate within aggregate is just a minor inconvenience.一切正常,聚合中的聚合只是一个小小的不便。

I deleted my previous answer, because I found a simpler solution.我删除了我之前的答案,因为我找到了一个更简单的解决方案。 This solution does not involve the pesky ContinueWith method or the TaskCompletionSource type.此解决方案不涉及讨厌的ContinueWith方法或TaskCompletionSource类型。 The idea is to return a nested Task<Task> from a local function , and Unwrap() it from the outer container function.这个想法是从本地函数返回嵌套的Task<Task> ,并从外部容器函数Unwrap()返回它。 Here is a basic outline of this idea:这是这个想法的基本概述:

public Task<T[]> GetAllAsync<T>()
{
    return LocalAsyncFunction().Unwrap();

    async Task<Task<T[]>> LocalAsyncFunction()
    {
        var tasks = new List<Task<T>>();
        // ...
        await SomethingAsync();
        // ...
        Task<T[]> whenAll = Task.WhenAll(tasks);
        return whenAll;
    }
}

The GetAllAsync method is not async . GetAllAsync方法不是async It delegates all the work to the LocalAsyncFunction , which is async , and then Unwrap s the resulting nested task and returns it.它将所有工作委托给asyncLocalAsyncFunction ,然后Unwrap生成嵌套任务并将其返回。 The unwrapped task contains in its .Exception.InnerExceptions property all the exceptions of the tasks , because it is just a facade of the internal Task.WhenAll task.展开的任务在其.Exception.InnerExceptions属性中包含tasks的所有异常,因为它只是内部Task.WhenAll任务的外观。

Let's demonstrate a more practical realization of this idea.让我们展示这个想法的更实际的实现。 The AsParallelUntilAsync method below enumerates lazily the source sequence and projects the items it contains to Task<TResult> s, until an item satisfies the predicate .下面的AsParallelUntilAsync方法懒惰地枚举source序列并将它包含的项目投影到Task<TResult> s,直到一个项目满足predicate It also limits the concurrency of the asynchronous operations.它还限制了异步操作的并发性。 The difficulty is that enumerating the IEnumerable<TSource> could throw an exception too.困难在于枚举IEnumerable<TSource>也可能引发异常。 The correct behavior in this case is to await all the running tasks before propagating the enumeration error, and return an AggregateException that contains both the enumeration error, and all the task errors that may have occurred in the meantime.在这种情况下,正确的行为是在传播枚举错误之前等待所有正在运行的任务,并返回一个包含枚举错误和同时可能发生的所有任务错误的AggregateException Here is how it can be done:这是如何完成的:

public static Task<TResult[]> AsParallelUntilAsync<TSource, TResult>(
    this IEnumerable<TSource> source, Func<TSource, Task<TResult>> action,
    Func<TSource, bool> predicate, int maxConcurrency)
{
    return Implementation().Unwrap();

    async Task<Task<TResult[]>> Implementation()
    {
        var tasks = new List<Task<TResult>>();

        async Task<TResult> EnumerateAsync()
        {
            var semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
            using var enumerator = source.GetEnumerator();
            while (true)
            {
                await semaphore.WaitAsync();
                if (!enumerator.MoveNext()) break;
                var item = enumerator.Current;
                if (predicate(item)) break;

                async Task<TResult> RunAndRelease(TSource item)
                {
                    try { return await action(item); }
                    finally { semaphore.Release(); }
                }

                tasks.Add(RunAndRelease(item));
            }
            return default; // A dummy value that will never be returned
        }

        Task<TResult> enumerateTask = EnumerateAsync();

        try
        {
            await enumerateTask; // Make sure that the enumeration succeeded
            Task<TResult[]> whenAll = Task.WhenAll(tasks);
            await whenAll; // Make sure that all the tasks succeeded
            return whenAll;
        }
        catch
        {
            // Return a faulted task that contains ALL the errors!
            return Task.WhenAll(tasks.Prepend(enumerateTask));
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM