4
\$\begingroup\$

Consider the pattern below, where multiple async calls are made in parallel:

var thisTask = ThisAsync();
var thatTask = ThatAsync();

await Task.WhenAll(thisTask, thatTask);

var this = await thisTask;
var that = await thatTask;

While it works fine, I find it a bit too verbose for what it is doing.

While having some fun with tuple types, I came up with the following approach that results in the same behavior:

var (this, that) = await (ThisAsync(), ThatAsync()).ResultsAsync();

It relies on an extension method on a tuple of Tasks, like below:

static class ParallelTasksExtensions
{
    static async Task<(T1, T2)> ResultsAsync<T1, T2>(this (Task<T1>, Task<T2>) tasks)
    {
        await Task.WhenAll(tasks.Item1, tasks.Item2);

        return (await tasks.Item1, await tasks.Item2);
    }
}

With a couple overloads (wish C# supported variadic generic types like C++ does in templates) this can be extended to an arbitrary number of parallel calls, so that we can have something like this:

var (one, two, three, four) = await (OneAsync(), TwoAsync(), ThreeAsync(), FourAsync()).ResultsAsync();

I like it quite a bit because of the following reasons:

  • Is is dramatically shorter. Usually shorter code leads to less chance of problems (not a golden rule of course, but I feel it applies here)
  • It requires less state to be maintained (less variables)
  • It avoids people calling .Result instead of await on the original tasks (a common bad practice)

Another option that would result in the same behavior would be to create an extension class instead of coding this as an extension method.

For example:

var (this, that) = await TaskEx.WhenAll((ThisAsync(), ThatAsync()));

This approach would be more inline with existing, non-tuple based overloads of Task.WhenAll.

Thoughts?

  • Is it "too clever" in your opinion? I don't think there is any similar native construct that achieves this so it could be seen as clever by some I'm sure.
  • Do you consider this over-engineering?
  • Is it more readable/intuitive to you?
  • Do you have other suggestions for the method name?
\$\endgroup\$
1
  • \$\begingroup\$ Is there even a reason for the await WhenAll? C# tasks run from the moment they're started, so they would still run in parallel even if you await them individually. \$\endgroup\$ Commented Oct 11, 2018 at 7:41

2 Answers 2

3
\$\begingroup\$

I've been doing something similar to your await Task.WhenAll, though not often enough to think about a shorter way of doing it. However, I've recently come to doubt the necessity of it.

Note that there is almost zero difference in the effective behavior of this code:

var t1 = SomethingAsync();
var t2 = OtherAsync();
await Task.WhenAll(t1, t2);
var r1 = await t1;
var r2 = await t2;

and this code:

var t1 = SomethingAsync();
var t2 = OtherAsync();
var r1 = await t1;
var r2 = await t2;

In both cases, the tasks are started. In both cases, the tasks run in the background as much as possible. In both cases, the method won't finish until both tasks are done, assuming they succeed.

If only task t1 fails, both cases will throw its exception. The first version will first wait for task t2 to finish, the second version will not. This may make a difference, but usually won't.

If only task t2 fails, both cases will throw its exception, although the second version will execute a little more code first; this difference is very unlikely to make a practical difference.

If both tasks fail, both cases will throw the exception of t1 and swallow the one of t2, see here: https://blogs.msdn.microsoft.com/seteplia/2018/02/14/one-user-scenario-to-rule-them-all/ down at "Exception Handling".

So if I don't mind the swallowed exceptions, I would simply omit the WhenAll completely, which makes your utility method redundant.

That said, it would make a good place to attach code that prevents the swallowing of the exception, if that is what you want. Given that, I like your idea, but it can be improved: you can make a tuple of tasks directly awaitable. The GetAwaiter method that the compiler implicitly calls may be an extension method. That would result in this code:

static class ParallelTasksExtensions
{
    static TaskAwaiter<(T1, T2)> GetAwaiter<T1, T2>(this (Task<T1>, Task<T2>) tasks)
    {
        return AwaitAndPackage().GetAwaiter();

        async Task<(T1, T2)> AwaitAndPackage()
        {
            await Task.WhenAll(tasks.Item1, tasks.Item2);
            return (await tasks.Item1, await tasks.Item2);
        }
    }
}

(Warning: untested.)

Now you can just write await (t1, t2) directly. You can then proceed to overload for more arguments, of course.

Or instead you can install the TaskTupleAwaiter NuGet package, which contains exactly this code already. See also the enhancement request that led to this.

\$\endgroup\$
4
  • \$\begingroup\$ Now you can just write await (t1, t2) directly. - could you explain how this magic is possible? Or do you mean it's used to await the tuple returned by GetAwaiter? In this case it wouldn't be magic anymore ;-) \$\endgroup\$
    – t3chb0t
    Commented Oct 11, 2018 at 9:39
  • 1
    \$\begingroup\$ @t3chb0t When you write await something, the compiler basically turns this into a series of operations on the result of something.GetAwaiter(). You can make absolutely anything awaitable simply by providing an appropriate GetAwaiter method or extension method for it. \$\endgroup\$ Commented Oct 11, 2018 at 13:12
  • \$\begingroup\$ I'm not sure where I would need this yet but this is awsome! I bet it won't be long until I find a use-case for it ;-) \$\endgroup\$
    – t3chb0t
    Commented Oct 11, 2018 at 17:10
  • \$\begingroup\$ I had no idea it was possible to add an extension method to expose the GetAwaiter method, that looks really cool. I tried playing around with an async tuple deconstructor but one cannot use out arguments with async so that was not an option. What I wanted was exactly that syntax, await (FirstAsync(), SecondAsync());. As for the fact that dropping the WhenAll call would make such extension useless, I disagree. The fact that there are less variables involved is reason enough to justify the extension IMHO. \$\endgroup\$
    – julealgon
    Commented Oct 15, 2018 at 15:24
2
\$\begingroup\$

I am fairly new to Asynchronous programming in c# but I have infact programmed parallel systems using C++. From all of my experience dealing from novice to seasoned programmers, I normally try and write code in a way in which other people reading my code would understand it one shot.

Since .NET 4.5, async and await has provided programmers an intuitive and easy way to perform Asynchronous operations without having to deal with Synchronization context or Callbacks, the runtime does this pretty well for them.

Coming back to your questions :-

Is it "too clever" in your opinion? I don't think there is any similar native construct that achieves this so it could be seen as clever by some I'm sure.

I would say cleaver but not "too clever", it is using a simple extension method to do what you would otherwise do by writing a hefty, verbose code.

Do you consider this over-engineering?

Not at all. It is simple and quite readable even to me even though I have been using C# for only a year.

Is it more readable/intuitive to you?

It is to me, but some of my colleagues found this a little complicated. But trust me, it is not. Always better to reduce your code as much as possible.

Do you have other suggestions for the method name?

I am not seasoned enough to answer a better solution than this.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.