NeilMacMullen
NeilMacMullen

Reputation: 3467

Can new C# language features be used to to clean-up Task.WhenAll syntax?

With "async everywhere", the ability to fire off multiple heterogeneous operations is becoming more frequent. The current Task.WhenAll method returns its results as an array and requires all tasks to return the same kind of object which makes its usage a bit clumsy. I'd like to be able to write...

var (i, s, ...) = await AsyncExtensions.WhenAll(
                          GetAnIntFromARemoteServiceAsync(),
                          GetAStringFromARemoteServiceAsync(),
                          ... arbitrary list of tasks   
                         );
Console.WriteLine($"Generated int {i} and string {s} ... and other things");

The best implementation I've been able to come up with is

public static class AsyncExtensions
{
  public static async Task<(TA , TB )> WhenAll<TA, TB>(Task<TA> operation1, Task<TB> operation2)
  {
             return (await operation1, await operation2;
  }
}

This has the disadvantage that I need to implement separate methods of up to N parameters. According to this answer that's just a limitation of using generics. This implementation also has the limitation that void-returning Tasks can't be supported but that is less of a concern.

My question is: Do any of the forthcoming language features allow a cleaner approach to this?

Upvotes: 4

Views: 342

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43554

As of .NET 6, there is no API available in the standard libraries that allows to await multiple heterogeneous tasks, and get their results in a value tuple.

I would like to point out though, and this is the main point of this answer, that the implementation that you've shown inside the question is incorrect.

// Incorrect
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
    return (await task1, await task2);
}

This is not WhenAll. This is WhenAllIfSuccessful_Or_WhenFirstFails. If the task1 fails, the error will be propagated immediately, and the task2 will become a fire-and-forget task. In some cases this might be exactly what you want. But normally you don't want to lose track of your tasks, and let them running unobserved in the background. You want to wait for all of them to complete, before continuing with the next step of your work. Here is a better way to implement the WhenAll method:

// Good enough
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
    await Task.WhenAll(task1, task2).ConfigureAwait(false);
    return (task1.Result, task2.Result);
}

This will wait for both tasks to complete, and in case of failure it will propagate the error of the first failed task (the first in the list of the arguments, not in chronological order). In most cases this is perfectly fine. But if you find yourself in a situation that requires the propagation of all exceptions, it becomes tricky. Below is the shortest implementation I know that imitates precisely the behavior of the native Task.WhenAll:

// Best
public static Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
    return Task.WhenAll(task1, task2).ContinueWith(t =>
    {
        if (t.IsFaulted)
        {
            TaskCompletionSource<(T1, T2)> tcs = new();
            tcs.SetException(t.Exception.InnerExceptions);
            return tcs.Task;
        }
        if (t.IsCanceled)
        {
            TaskCompletionSource<(T1, T2)> tcs = new();
            tcs.SetCanceled(new TaskCanceledException(t).CancellationToken);
            return tcs.Task;
        }
        Debug.Assert(t.IsCompletedSuccessfully);
        return Task.FromResult((task1.Result, task2.Result));
    }, default, TaskContinuationOptions.DenyChildAttach |
        TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Unwrap();
}

Upvotes: 1

BatteryBackupUnit
BatteryBackupUnit

Reputation: 13233

There's an open feature-request for this on the dotnet/csharplang repository.

The issue also mentions another open feature request, tuple splatting which could help, to some extent. How, is explained here.

Both issues are currently labeled as [Discussion] and [Feature Request] and have been "idle" for a year now (May 2017 - May 2018).

Hence I'd deduce that the answer (currently) is "no".


Going the extension way Joseph Musser did write up a load of these for us to copy & paste: https://gist.github.com/jnm2/3660db29457d391a34151f764bfe6ef7

Upvotes: 5

Related Questions