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

梦想与她 提交于 2020-05-23 05:23:09

问题


Given the following:

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

the call to task.Wait() throws an AggregateException, whose inner exceptions contain the fail1 and fail2 exceptions. But how can I access the tPass1 successful result?

Is this possible?

I'm aware that I can get the result from the individual task after the WhenAll has finished, via tPass1.Result however is there a way to get them in an array to avoid having to manually track all the things feeding into the WhenAll?


回答1:


When a task fails we cannot access its 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 struct.

public static Task<(T[] Results, Exception[] Exceptions)> WhenAllEx<T>(params Task<T>[] tasks)
{
    return Task.WhenAll(tasks).ContinueWith(_ => // 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(); // Trick to flatten the hierarchy of AggregateExceptions
        return (results, exceptions);
    }, TaskContinuationOptions.ExecuteSynchronously);
}

Usage example:

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

Output:

Status: RanToCompletion
Results: 1
Exceptions: fail1, fail2


Caution: the implementation above ignores canceled tasks, and so does not mimic the default behavior of Task.WhenAll:

If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.

A more consistent behavior would be to return a single TaskCanceledException in this case. This can be done by adding this code inside the WhenAllEx, just before the final return command:

if (exceptions.Length == 0)
{
    var canceledTask = tasks.FirstOrDefault(t => t.IsCanceled);
    if (canceledTask != null)
    {
        exceptions = new[] { new TaskCanceledException(canceledTask) };
    }
}



回答2:


Maybe

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

Usage

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] ]



回答3:


Playing around with @Theodor Zoulias's powerfull and elegant solution pushed me to something. It looks hacky, but still works. One can continue Task.WhenAll with something that will not throw an exception for sure (e.g. _ => { }) and Wait that something.

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

Output looks like this:

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



回答4:


Change

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

to

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

Working example



来源:https://stackoverflow.com/questions/55887028/is-it-possible-to-get-successful-results-from-a-task-whenall-when-one-of-the-tas

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!