Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: Task.WhenAll to return results in a strongly typed tuple #100504

Open
petroemil opened this issue Apr 2, 2024 · 2 comments
Open
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading.Tasks needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Milestone

Comments

@petroemil
Copy link

petroemil commented Apr 2, 2024

Background and motivation

A very common scenario is to run multiple asynchronous operations in parallel and continue once all of them returned their results. Currently, the only API we have for such scenarios is the Task.WhenAll, but it loses the type information of the individual Tasks because it operates on an array of Tasks of the same type.

If the Tasks are of different types, await-ing them in parallel and capturing their results becomes cumbersome.

With the ability to deconstruct tuples at assignment, there could be an API to run multiple Tasks of different types in parallel and return their results as a tuple which can be deconstructed immediately without having to hold on to intermediate Task<T> references.

API Proposal

namespace System.Threading.Tasks;

class Task
{
    public static Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        // note: I'm pretty sure there's a more efficient implementation than this
        return Task.WhenAll(task1, task2).ContinueWith(_ => (task1.Result, task2.Result));
    }

    public static Task<(T1, T2, T3)> WhenAll<T1, T2, T3>(Task<T1> task1, Task<T2> task2, Task<T3> task3);
    public static Task<(T1, T2, T3, T4)> WhenAll<T1, T2, T3, T4>(Task<T1> task1, Task<T2> task2, Task<T3> task3, Task<T4> task4);
    public static Task<(T1, T2, T3, T4, T5)> WhenAll<T1, T2, T3, T4, T5>(Task<T1> task1, Task<T2> task2, Task<T3> task3, Task<T4> task4, Task<T5> task5);
}

API Usage

async Task<string> GetStringAsync() => "Hello World";
async Task<int> GetIntAsync() => 42;

var (s, i) = await Task.WhenAll(
    GetStringAsync(), 
    GetIntAsync());

Alternative Designs

I know that this wouldn't quite work because the Task.WhenAll API already has params overload, so there would be ambiguity or unwanted preference between the overloads, but maybe the provided Task<T> parameters could be wrapped in a ValueTuple to avoid that overload collision, which would look something like this:

public static Task<(T1, T2)> WhenAll<T1, T2>((Task<T1>, Task<T2>) tasks);

I'll admit, this does look a little weird from the caller's perspective though.

Another alternative is to use a different method name, such as WhenEach (though it rather rhymes with foreach and is a new API planned for .NET 9 ( #61959 ) returning Tasks via an IAsyncEnumerable as they complete, so returning a Tuple / ValueTuple there might be confusing, plus, I'd assume it will also have a params overload and would meet with the same problem as the WhenAll API).

So, WhenEvery, WhenSome, WhenAllIndividual?

@petroemil petroemil added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Apr 2, 2024
@zlatanov
Copy link
Contributor

zlatanov commented Apr 3, 2024

Here is something better for you:

public static class TaskExtensions
{
    public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks)
    {
        return Task.WhenAll(tasks.Item1, tasks.Item2)
                   .ContinueWith(_ => Task.FromResult((tasks.Item1.Result, tasks.Item2.Result)))
                   .Unwrap()
                   .GetAwaiter();
    }
}

Now you can do this instead:

var t1 = Task.Run(() => 1);
var t2 = Task.Run(() => "2");

var (r1, r2) = await (t1, t2);

Of course, you can try and write a custom TaskAwaiter to avoid the extra allocations, but I doubt it would matter much.

@petroemil
Copy link
Author

petroemil commented Apr 4, 2024

Thanks, I've seen this trick before, but while it looks cool, I think it could easily confuse less experienced developers, why you could await a tuple. The same way we don't have implicit GetAwaiter extension method for Task arrays, I don't think it's a good idea to introduce something like this at the BCL level.

With the proposed API in place, you could have this implicit tuple awaiter with even less effort and potentially error-prone implementation. Your TaskExtensions example would be reduced to

public static class TaskExtensions
{
    public static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks)
    {
        return Task.WhenAll(tasks.Item1, tasks.Item2).GetAwaiter();
    }
}

@ericstj ericstj added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Jul 18, 2024
@ericstj ericstj added this to the Future milestone Jul 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading.Tasks needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Projects
None yet
Development

No branches or pull requests

3 participants