簡體   English   中英

如何從異步方法返回 AggregateException

[英]How to return AggregateException from async method

我得到了一個像增強的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 ,異常是具有 2 個內部異常的AggregateException

對於MyWhenAll ,異常是AggregateException ,其中包含 1 個內部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();

一切正常,聚合中的聚合只是一個小小的不便。

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

使用TaskCompletionSource

最外層的異常由.Wait().Result創建 - 這被記錄為將存儲在 Task 中的異常包裝在AggregateException中(以保留其堆棧跟蹤 - 這是在創建ExceptionDispatchInfo之前引入的)。

但是,Task 實際上可以包含許多異常。 在這種情況下, .Wait().Result將拋出一個包含多個InnerExceptionsAggregateException 您可以通過TaskCompletionSource.SetException(IEnumerable<Exception> exceptions)訪問此功能。

所以您不想創建自己的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.WhenAll返回的Task

我得到了一個像增強的Task.WhenAll一樣工作的異步方法。 它需要一堆任務,並在所有任務完成后返回。

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

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

我的問題是,當一個或多個任務失敗時,如何獲得返回Task.WhenAll返回的 Task 的方法?

如果我收集異常並拋出一個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帶有 1 個內部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();

一切正常,聚合中的聚合只是一個小小的不便。

我刪除了我之前的答案,因為我找到了一個更簡單的解決方案。 此解決方案不涉及討厭的ContinueWith方法或TaskCompletionSource類型。 這個想法是從本地函數返回嵌套的Task<Task> ,並從外部容器函數Unwrap()返回它。 這是這個想法的基本概述:

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 它將所有工作委托給asyncLocalAsyncFunction ,然后Unwrap生成嵌套任務並將其返回。 展開的任務在其.Exception.InnerExceptions屬性中包含tasks的所有異常,因為它只是內部Task.WhenAll任務的外觀。

讓我們展示這個想法的更實際的實現。 下面的AsParallelUntilAsync方法懶惰地枚舉source序列並將它包含的項目投影到Task<TResult> s,直到一個項目滿足predicate 它還限制了異步操作的並發性。 困難在於枚舉IEnumerable<TSource>也可能引發異常。 在這種情況下,正確的行為是在傳播枚舉錯誤之前等待所有正在運行的任務,並返回一個包含枚舉錯誤和同時可能發生的所有任務錯誤的AggregateException 這是如何完成的:

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