4

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?

NeilMacMullen
  • 3,339
  • 2
  • 20
  • 22
  • have you tried something like `return (await GetAnIntFromARemoteServiceAsync, await GetAStringFromARemoteServiceAsync);` ? – BatteryBackupUnit May 07 '18 at 15:09
  • 1
    Your `WhenAll` overload can be re-written to just be `public static async Task<(TA a, TB b)> WhenAll(Task operation1, Task operation2) { return (await operation1, await operation2); }` This avoids creating of a while bunch of new objects, a bunch of casts, and is just plain easier to read. (And it scales up as you increase objects just fine). – Servy May 07 '18 at 15:12
  • 1
    @BatteryBackupUnit That wouldn't start the second operation until the first was complete. – Servy May 07 '18 at 15:12
  • @Servy Are you sure about that? I would have thought that would effectively remove the parallelisation introduced by WhenAll. (Sorry - maybe you were referring to BatteryBackupUnit's answer, pre-edit? – NeilMacMullen May 07 '18 at 15:14
  • 1
    @NeilMacMullen The tasks are already running by the time you get to the body of your new method (you accept tasks, not functions that produce tasks), so you couldn't remove the parallelism even if you tried at that point. `WhenAll` doesn't introduce parallelism at all. It just creates a new tasks that is completed when all of the provided tasks are done. Awaiting all of them in turn does exactly the same thing. – Servy May 07 '18 at 15:14
  • 1
    There's an [issue for this feature on the dotnet/csharplang repo](https://github.com/dotnet/csharplang/issues/380) – BatteryBackupUnit May 07 '18 at 15:20
  • 3
    And [here's the code that someone wrote for you](https://gist.github.com/jnm2/3660db29457d391a34151f764bfe6ef7), and nuget package [TaskTupleAwaiter](https://packages.nuget.org/packages/TaskTupleAwaiter/1.1.0) – BatteryBackupUnit May 07 '18 at 15:23
  • Servy - yes, you are right. @BatteryBackupUnit thanks - nice to know other people are thinking about this. – NeilMacMullen May 07 '18 at 15:25
  • 1
    I never even realized that Task.WhenAll returned results. I always operate on the tasks list after I await the WhenAll call. – Stilgar May 07 '18 at 15:32

2 Answers2

5

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

BatteryBackupUnit
  • 12,934
  • 1
  • 42
  • 68
1

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();
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104