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

后端 未结 4 650
陌清茗
陌清茗 2021-01-03 10:22

Given the following:

var tPass1 = Task.FromResult(1);
var tFail1 = Task.FromException(new ArgumentException(\"fail1\"));
var tFail2 = Task.FromExc         


        
相关标签:
4条回答
  • 2021-01-03 10:29

    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

    0 讨论(0)
  • 2021-01-03 10:49

    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
    
    0 讨论(0)
  • 2021-01-03 10:50

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

    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

    0 讨论(0)
  • 2021-01-03 10:53

    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] ]
    
    0 讨论(0)
提交回复
热议问题