忽略在 Task.WhenAll 抛出异常的任务,只获取完成的结果

Posted

技术标签:

【中文标题】忽略在 Task.WhenAll 抛出异常的任务,只获取完成的结果【英文标题】:Ignore the Tasks throwing Exceptions at Task.WhenAll and get only the completed results 【发布时间】:2017-01-28 02:46:12 【问题描述】:

我正在处理一个任务并行问题,我有许多任务可能会或可能不会抛出异常。

我想处理所有正确完成的任务并记录其余的。 Task.WhenAll 传播任务异常,但不允许我收集其余结果。

    static readonly Task<string> NormalTask1 = Task.FromResult("Task result 1");
    static readonly Task<string> NormalTask2 = Task.FromResult("Task result 2");
    static readonly Task<string> ExceptionTk = Task.FromException<string>(new Exception("Bad Task"));
    var results = await Task.WhenAll(new [] NormalTask1,NormalTask2,ExceptionTk);

Task.WhenAll 抛出 ExcceptionTk 的异常,忽略其余结果。如何在忽略异常的情况下获得结果并同时记录异常?

我可以将任务包装到另一个任务中,try...catch()... 内部异常但我无权访问它们,我希望我不必添加此开销。

【问题讨论】:

您可以检查每个任务的状态。你会发现 NormalTask​​1 和 NormalTask​​2 是 RanToCompletion 而 ExceptionTk 是 Faulted。 捕获异常没有任何开销。如果异常频繁到您想忽略它们,您可以使用包装函数将所有结果转换为Result&lt;TSuccess,TFailure&gt; 类型并统一处理它们。这是面向铁路的编程风格的一部分 @PanagiotisKanavos 开销用于任务包装,而不是用于捕获异常。 糟糕,没有注意到您调用的是WaitAll 而不是WhenAll。无论如何,使用WaitAll,您已经进行了昂贵的阻塞操作。在任何情况下,将响应包装在结果类中都是更好的选择。我怀疑您有处理任务的管道,在这种情况下,您还应该查看 TPL 数据流 【参考方案1】:

你可以创建一个这样的方法来代替Task.WhenAll

public Task<ResultOrException<T>[]> WhenAllOrException<T>(IEnumerable<Task<T>> tasks)
    
    return Task.WhenAll(
        tasks.Select(
            task => task.ContinueWith(
                t => t.IsFaulted
                    ? new ResultOrException<T>(t.Exception)
                    : new ResultOrException<T>(t.Result))));



public class ResultOrException<T>

    public ResultOrException(T result)
    
        IsSuccess = true;
        Result = result;
    

    public ResultOrException(Exception ex)
    
        IsSuccess = false;
        Exception = ex;
    

    public bool IsSuccess  get; 
    public T Result  get; 
    public Exception Exception  get; 

然后你可以检查每个结果,看看是否成功。


编辑:上面的代码不处理取消;这是另一种实现:

public Task<ResultOrException<T>[]> WhenAllOrException<T>(IEnumerable<Task<T>> tasks)
    
    return Task.WhenAll(tasks.Select(task => WrapResultOrException(task)));


private async Task<ResultOrException<T>> WrapResultOrException<T>(Task<T> task)

    try
               
        var result = await task;
        return new ResultOrException<T>(result);
    
    catch (Exception ex)
    
        return new ResultOrException<T>(ex);
    

【讨论】:

这几乎就像面向铁路的编程。也许 OP 应该考虑更改原始函数本身以返回 Result&lt;TSuccess,TFailure&gt; 值,或者使用包装函数将所有任务结果转换为 Result&lt;&gt; 如果一个任务被取消了怎么办?如果您也想处理这种情况,请将t.IsFaulted 替换为t.Exception != null @svick 好点。检查t.Exception 不起作用,因为已取消的任务可能没有设置异常(只需检查Task.FromCanceled)。请参阅我的更新答案。 一个可能的改进是使用 TaskContinuationOptions 为故障、取消和完成的延续定义单独的延续。这将使处理单独的情况变得更容易,并摆脱条件 我用你回答的第二部分来处理错误,谢谢。【参考方案2】:

您可以从其属性Result 中获取每个成功完成的Task&lt;TResult&gt; 的结果。

var NormalTask1 = Task.FromResult("Task result 1");
var NormalTask2 = Task.FromResult("Task result 2");
var ExceptionTk = Task.FromException<string>(new Exception("Bad Task"));

Task<string>[] tasks = new[]  NormalTask1, NormalTask2, ExceptionTk ;
Task whenAll = Task.WhenAll(tasks);
try

    await whenAll;

catch

    if (whenAll.IsFaulted) // there is also the possibility of being canceled
    
        foreach (var ex in whenAll.Exception.InnerExceptions)
        
            Console.WriteLine(ex); // Log each exception
        
    


string[] results = tasks
    .Where(t => t.Status == TaskStatus.RanToCompletion)
    .Select(t => t.Result)
    .ToArray();
Console.WriteLine($"Results: String.Join(", ", results)");

输出:

System.Exception:错误的任务 结果:任务结果1,任务结果2

【讨论】:

谢谢你,Thodore,我没有测试你的代码,但我想当抛出异常时,WaitAll 退出,catch 处理日志记录。到那时,其他任务可能还没有返回,当我尝试收集结果时,一切似乎都在运行或取消。尝试在NormalTasks (Thread.Sleep) 上增加一些负载。 嗨@MenelaosVergis! Task.WaitAll 等待直到所有任务都已完成,成功或不成功(状态为故障或取消)。也许你把它和Task.WaitAny混淆了? 你说得对,我把它和一些东西混淆了,不是Task.WaitAny,而是Task.WhenAll @MenelaosVergis 你可以看看这个问题:WaitAll vs WhenAll。通常Task.WhenAllTask.WaitAll 的使用方式不同。如果您只是在 try-catch 块中使用await Task.WhenAll,您将只观察到一个异常。此行为是由await 运算符引起的。但是可以观察到所有的例外情况。只需将Task.WhenAll 返回的任务存储在一个变量中,然后在 catch 块中检查其Exception 属性。 是的,我自己也经历过,这就是我提出这个问题的原因。再次阅读问题,我已将拼写错误从WaitAll 更新为WhenAll【参考方案3】:

您可以添加带有异常处理的 HOC,然后检查是否成功。

 class Program

    static async Task Main(string[] args)
    
        var itemsToProcess = new[]  "one", "two" ;
        var results = itemsToProcess.ToDictionary(x => x, async (item) =>
        
            try
            
                var result = await DoAsync();
                return ((Exception)null, result);
            
            catch (Exception ex)
            
                return (ex, (object)null);
            
        );

        await Task.WhenAll(results.Values);

        foreach(var item in results)
        
            Console.WriteLine(item.Key + (await item.Value).Item1 != null ? " Failed" : "Succeed");
        
    

    public static async Task<object> DoAsync()
    
        await Task.Delay(10);
        throw new InvalidOperationException();
    

【讨论】:

以上是关于忽略在 Task.WhenAll 抛出异常的任务,只获取完成的结果的主要内容,如果未能解决你的问题,请参考以下文章

为啥不等待 Task.WhenAll 抛出 AggregateException?

C# 使用AggregateException 信息

Task.WhenAll 不等待任务完成[关闭]

c# Task.WhenAll(tasks) 和 SemaphoreSlim - 如何知道所有任务何时已完全完成

Task CancellationTokenSource和Task.WhenAll的应用

Parallel.ForEach 与 Task.Run 和 Task.WhenAll