首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何从异步方法返回AggregateException

如何从异步方法返回AggregateException
EN

Stack Overflow用户
提问于 2019-04-15 10:58:01
回答 3查看 1.5K关注 0票数 1

我得到了一个像增强的Task.WhenAll一样工作的异步方法。当所有任务都完成时,它需要大量的任务和返回。

代码语言:javascript
复制
public async Task MyWhenAll(Task[] tasks) {
    ...
    await Something();
    ...

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

我的问题是,当一个或多个任务失败时,如何获得返回类似于Task.WhenAll返回的任务的方法?

如果我收集异常并抛出一个AggregateException,它将被包装在另一个AggregateException中。

编辑:完整示例

代码语言:javascript
复制
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,除2个内部异常外,AggregateException为例外。

对于MyWhenAll,异常是AggregateException,有一个内部AggregateException和两个内部异常。

编辑:为什么我要做这个

我经常需要调用分页API:s,并且希望限制同时连接的数量。

实际的方法签名是

代码语言:javascript
复制
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)

这意味着我可以像这样分页

代码语言:javascript
复制
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();

这一切都很好,聚合中的聚合只是一个小小的不便。

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2019-04-15 15:38:49

async方法只设计为返回任务上的每个集合--最多是单个异常,而不是多个。

这给您留下了两个选项,您可以先不使用async方法,而是依赖其他执行方法的方法:

代码语言:javascript
复制
public Task MyWhenAll(Task t1, Task t2)
{
    return Task.Delay(TimeSpan.FromMilliseconds(100))
        .ContinueWith(_ => Task.WhenAll(t1, t2))
        .Unwrap();
}

如果您有一个更复杂的方法,在不使用await的情况下很难编写,那么您需要打开嵌套的聚合异常,这虽然不是非常复杂,但却很繁琐:

代码语言:javascript
复制
    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;
    }
票数 3
EN

Stack Overflow用户

发布于 2019-04-15 15:18:03

使用TaskCompletionSource

最外层的异常是由.Wait().Result创建的--这被记录为将存储在任务中的异常包装在AggregateException中(为了保存其堆栈跟踪--这是在创建ExceptionDispatchInfo之前引入的)。

但是,任务实际上可以包含许多异常。在这种情况下,.Wait().Result将抛出一个包含多个InnerExceptionsAggregateException。您可以通过TaskCompletionSource.SetException(IEnumerable exceptions)访问此功能。

因此,您不希望创建自己的AggregateException。在任务上设置多个异常,让.Wait().Result为您创建该AggregateException

所以:

代码语言:javascript
复制
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。最后,您会做一些像这样的丑事(从您的问题中调整示例代码):

代码语言:javascript
复制
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.WhenAll返回的Task.WhenAll

票数 0
EN

Stack Overflow用户

发布于 2021-09-21 23:07:11

我删除了以前的答案,因为我找到了一个更简单的解决方案。此解决方案不涉及烦人的ContinueWith方法或TaskCompletionSource类型。其思想是从一个Task<Task>返回一个嵌套的局部函数,从外部容器函数返回一个Unwrap()。以下是这个想法的基本纲要:

代码语言:javascript
复制
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;
    }
}

GetAllAsync方法不是async。它将所有工作委托给LocalAsyncFunction (即async ),然后Unwrap对生成的嵌套任务进行赋值并返回它。未包装的任务在其.Exception.InnerExceptions属性中包含tasks的所有异常,因为它只是内部Task.WhenAll任务的外观。

让我们演示一下这个想法的一个更实际的实现。下面的AsParallelUntilAsync方法懒散地枚举source序列,并将其包含的项投影到Task<TResult>s,直到一个项满足predicate。它还限制了异步操作的并发性。困难在于枚举IEnumerable<TSource>也可能引发异常。在这种情况下,正确的行为是在传播枚举错误之前等待所有正在运行的任务,并返回包含枚举错误和可能同时发生的所有任务错误的AggregateException。以下是如何做到这一点:

代码语言:javascript
复制
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));
        }
    }
}
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/55687698

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档