当其中一项任务失败时,是否有可能从 Task.WhenAll 获得成功的结果?

Is it possible to get successful results from a Task.WhenAll when one of the tasks fails?

鉴于以下情况:

var tPass1 = Task.FromResult(1);
var tFail1 = Task.FromException<int>(new ArgumentException("fail1"));
var tFail2 = Task.FromException<int>(new ArgumentException("fail2"));

var task = Task.WhenAll(tPass1, tFail1, tFail2);
task.Wait();

对 task.Wait() 的调用抛出 AggregateException,其内部异常包含 fail1fail2 异常。但是如何访问 tPass1 成功结果?

这可能吗?

我知道我可以在 WhenAll 完成后通过 tPass1.Result 从单个任务中获取结果,但是有没有办法将它们放入数组中以避免必须手动跟踪所有输入 WhenAll?

的东西

可能

public async Task<Task[]> RejectFailedFrom(params Task[] tasks)
{
    try
    {
        await Task.WhenAll(tasks);
    }
    catch(Exception exception)
    {
        // Handle failed tasks maybe
    }

    return tasks.Where(task => task.Status == TaskStatus.RanToCompletion).ToArray();
}

用法

var tasks = new[]
{
    Task.FromResult(1),
    Task.FromException<int>(new ArgumentException("fail1")),
    Task.FromException<int>(new ArgumentException("fail2"))
};

var succeed = await RejectFailedFrom(tasks);
// [ tasks[0] ]

当任务失败时,我们无法访问其 Result property because it throws. So to have the results of a partially successful WhenAll task, we must ensure that the task will complete successfully. The problem then becomes what to do with the exceptions of the failed internal tasks. Swallowing them is probably not a good idea. At least we would like to log them. Here is an implementation of an alternative WhenAll that never throws, but returns both the results and the exceptions in a ValueTuple 结构。

public static Task<(T[] Results, Exception[] Exceptions)> WhenAllEx<T>(
    params Task<T>[] tasks)
{
    tasks = tasks.ToArray(); // Defensive copy
    return Task.WhenAll(tasks).ContinueWith(t => // return a continuation of WhenAll
    {
        var results = tasks
            .Where(t => t.Status == TaskStatus.RanToCompletion)
            .Select(t => t.Result)
            .ToArray();
        var aggregateExceptions = tasks
            .Where(t => t.IsFaulted)
            .Select(t => t.Exception) // The Exception is of type AggregateException
            .ToArray();
        var exceptions = new AggregateException(aggregateExceptions).Flatten()
            .InnerExceptions.ToArray(); // Flatten the hierarchy of AggregateExceptions
        if (exceptions.Length == 0 && t.IsCanceled)
        {
            // No exceptions and at least one task was canceled
            exceptions = new[] { new TaskCanceledException(t) };
        }
        return (results, exceptions);
    }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

用法示例:

var tPass1 = Task.FromResult(1);
var tFail1 = Task.FromException<int>(new ArgumentException("fail1"));
var tFail2 = Task.FromException<int>(new ArgumentException("fail2"));

var task = WhenAllEx(tPass1, tFail1, tFail2);
task.Wait();
Console.WriteLine($"Status: {task.Status}");
Console.WriteLine($"Results: {String.Join(", ", task.Result.Results)}");
Console.WriteLine($"Exceptions: {String.Join(", ", task.Result.Exceptions.Select(ex => ex.Message))}");

输出:

Status: RanToCompletion
Results: 1
Exceptions: fail1, fail2

玩弄@Theodor Zoulias 强大而优雅的 让我有所收获。它看起来很老套,但仍然有效。可以继续 Task.WhenAll 肯定不会抛出异常的东西(例如 _ => { })和 Wait 那东西。

var cts = new CancellationTokenSource();
cts.Cancel();
var canceled = Task.Run(() => 1, cts.Token);

var faulted = Task.FromException<int>(new Exception("Some Exception"));

var ranToCompletion = Task.FromResult(1);

var allTasks = new[] { canceled, faulted, ranToCompletion };

// wait all tasks to complete regardless anything
Task.WhenAll(allTasks).ContinueWith(_ => { }).Wait();

foreach(var t in allTasks)
{
    Console.WriteLine($"Task #{t.Id} {t.Status}");
    if (t.Status == TaskStatus.Faulted)
        foreach (var e in t.Exception.InnerExceptions)
            Console.WriteLine($"\t{e.Message}");
    if (t.Status == TaskStatus.RanToCompletion)
        Console.WriteLine($"\tResult: {t.Result}");
}

输出如下所示:

Task #2 Canceled
Task #1 Faulted
        Some Exception
Task #5 RanToCompletion
        Result: 1

改变

var task = Task.WhenAll(tPass1, tFail1, tFail2);
task.Wait();

var all = new Task<int>[] { tPass1, tFail1, tFail2 }
    .Where(t => t.Status == TaskStatus.RanToCompletion);
var task = Task.WhenAll(all);
task.Wait();

Working example

ConcurrentQueue<Exception> errorList = new();

var someTasks = makeSomeActionAsync().ContinueWith(x => 
{
   if(x.Exception !=null)
      errorList.Enqueue(x.Exception);
});

await Task.WhenAll(someTasks);

if(errorList.Any())
   throw new Exception($"\n{string.Join("\n", errorList )}");