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

23
我正在处理一个任务并行问题,其中有许多任务可能会抛出异常。
我想处理所有正常完成的任务并记录其余内容。 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(){...} 内部异常,但我无法访问它们,希望不必添加此开销。


您可以检查每个单独任务的状态。您会发现NormalTask1和NormalTask2是RanToCompletion,而ExceptionTk是Faulted。 - Dennis_E
捕获异常时不会涉及任何开销。如果异常非常频繁,您可以使用包装函数将所有结果转换为“Result<TSuccess,TFailure>”类型并统一处理它们。这是铁路导向编程风格的一部分。 - Panagiotis Kanavos
@PanagiotisKanavos 这个开销是为了任务包装而设计的,而不是为了捕获异常。 - Menelaos Vergis
1
哎呀,我没注意到你调用的是WaitAll而不是WhenAll。无论如何,使用WaitAll已经有一个昂贵的阻塞操作了。在任何情况下,将响应包装在结果类中是更好的选择。我猜你有一系列的处理任务,如果是这样的话,你还应该看看TPL Dataflow。 - Panagiotis Kanavos
3个回答

25
您可以创建一个类似于这样的方法来代替使用Task.WhenAll
您可以像这样创建一个方法来代替使用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);
    }
}

1
这几乎就像是面向铁路编程。也许OP应该考虑更改原始函数本身以返回Result<TSuccess,TFailure>值,或者使用包装器函数将所有任务结果转换为Result<> - Panagiotis Kanavos
如果一个任务被取消了怎么办?如果你也想处理这种情况,那么请将t.IsFaulted替换为t.Exception != null - svick
@svick 说得好。检查t.Exception是行不通的,因为取消的任务可能没有设置异常(我刚用Task.FromCanceled检查过了)。请看我的更新答案。 - Thomas Levesque
一个可能的改进是使用TaskContinuationOptions来定义故障、取消、完成后续的单独处理。这将使处理不同情况更容易,并且可以消除条件语句。 - Panagiotis Kanavos
我使用了你回答里的第二部分来处理错误,谢谢。 - Menelaos Vergis
一个 Task<TResult> 已经有了 ResultException 属性,这就质疑了是否需要像 ResultOrException 这样的类。 - Theodor Zoulias

6
你可以从每个成功完成的Task<TResult>的属性Result中获取结果。
Task<string> normalTask1 = Task.FromResult("Task result 1");
Task<string> normalTask2 = Task.FromResult("Task result 2");
Task<string> 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 (Exception ex in whenAll.Exception.InnerExceptions)
        {
            Console.WriteLine(ex); // Log each exception
        }
    }
}

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

输出:

System.Exception: Bad Task  
Results: Task result 1, Task result 2  

谢谢你,Theodore。我还没有测试你的代码,但我猜当异常被抛出时,WaitAll 退出并且 catch 处理日志记录。这时其他任务可能还没有返回,当我尝试收集结果时,似乎一切都在运行或取消。请尝试在 NormalTasks 上添加一些负载(Thread.Sleep)。 - Menelaos Vergis
嗨@MenelaosVergis!Task.WaitAll等待所有任务完成,无论成功还是失败(状态为故障或取消)。也许您将其与Task.WaitAny混淆了? - Theodor Zoulias
你说得对,我把它和Task.WaitAny混淆了,实际上是和Task.WhenAll混淆了。 - Menelaos Vergis
@MenelaosVergis 你可以看一下这个问题:WaitAll vs WhenAll。通常情况下,Task.WhenAllTask.WaitAll的使用方式不同。如果你只是在try-catch块中等待Task.WhenAll,你只会观察到一个异常。这种行为是由await操作符引起的。但是,你可以观察到所有的异常。只需将Task.WhenAll返回的任务存储在一个变量中,然后在catch块中检查它的Exception属性即可。 - Theodor Zoulias
简单多了,干得好! - Menelaos Vergis
显示剩余2条评论

0

您可以添加带有异常处理的高阶组件,然后检查成功。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接