5

I have to consume the output of multiple asynchronous tasks right after they complete.

Would there be a reasonable perf difference in any of these approaches?

Simple Await

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    foreach (Task<List<Baz>> task in tasks) {
        results.AddRange(await task);

    return results;
}

WhenAll

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    foreach (List<Baz> bazList in await Task.WhenAll(tasks))
        results.AddRange(bazList);

    return results;
}

WaitAll

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    foreach (List<Baz> bazList in await Task.WaitAll(tasks))
        results.AddRange(bazList);

    return results;
}

WhenAny

public async Task<List<Baz>> MyFunctionAsync(List<Foo> FooList) {
    results = new List<Baz>();
    List<Task<List<Baz>>> tasks = new List<Task<List<Baz>>>();

    foreach (Foo foo in FooList) {
        tasks.Add(FetchBazListFromFoo(entry));

    while (tasks.Count > 0) {
        Task<List<Baz>> finished = Task.WhenAny(tasks);
        results.AddRange(await finished);
        tasks.Remove(finished);
    }

    return results;
}
  • FooList has about 100 entries.
  • FetchBazListFromFoo makes about 30 REST API calls and does some synchronous work for each result of REST API call.

Additionally, Is there an internal overhead diff in WhenAll v WhenAny?

WhenAll returns control after all tasks are completed, while WhenAny returns control as soon as a single task is completed. The latter seems to require more internal management.

Nishant
  • 2,521
  • 1
  • 15
  • 29

2 Answers2

5

The third approach (WaitAll) is invalid because the Task.WaitAll is a void returning method, so it cannot be awaited. This code will just produce a compile-time error.

The other three approaches are very similar, with some subtle differences.

Simple Await: starts all tasks and then awaits them one-by-one. It will collect all results in the correct order. In case of an exception will return before all tasks are completed, and will report only the exception of the first failed task (first in order, not chronologically).
Not recommended unless this behavior is exactly what you want (most probably it isn't).

WhenAll: starts all tasks and then awaits all of them to complete. It will collect all results in the correct order. In case of an exception will return after all tasks have been completed, and will report only the exception of the first failed task (first in order, not chronologically).
Not recommended unless this behavior is exactly what you want (most probably it isn't either).

WhenAny: starts all tasks and then awaits all of them to complete. It will collect all results in order of completion, so the original order will not be preserved. In case of an exception will return immediately, and will report the exception of the first failed task (this time first chronologically, not in order). The while loop introduces an overhead that is absent from the other two approaches, which will be quite significant if the number of tasks is larger than 10,000, and will grow exponentially as the number of tasks becomes larger.
Not recommended unless this behavior is exactly what you want (I bet by now you should not be a fan of this either).

All of these approaches: will bombard the remote server with a huge number of concurrent requests, making it hard for that machine to respond quickly, and in the worst case triggering a defensive anti-DOS-attack mechanism.

The best solution to this problem is to discard all of these mediocre approaches, and use instead a library specialized for this kind of problems, which is embedded in .NET Core and available as a package for .NET Framework. It is the TPL Dataflow library, that allows to build a processing pipeline consisting of sync or async processing blocks, each one configured with its own degree of parallelism. You'll get optimal performance, you'll get control of how much load you put on the remote server, you'll utilize optimally the CPU resources of your local machine, you'll get the results in the correct order, and you'll get an exception shortly after the failure of the first task. As a bonus you also get the ability to cancel the operation at any time if you wish so. The disadvantage is the learning curve, which fortunately is not very steep. You will be able to use this library successfully after one day or two of studying.

Theodor Zoulias
  • 24,585
  • 5
  • 40
  • 69
  • Thanks for your answer. I do have an agreement with the server and am sure that the job will not be throttled. Will definitely look into TPL. – Nishant Mar 13 '20 at 05:37
  • Await, WhenAll, WhenAny do not start tasks. Tasks usually start hot (the only case when a task is not running is when he was created with `new Task` which however is not the recommended way of creating tasks). Await, WhenAll, WhenAny simply wait until one or more tasks have completed. – ckuri Mar 13 '20 at 05:39
  • @ckuri I am referring to the approaches that the OP named "Simple Await", "WhenAll" and "WhenAny". I am not referring to the keyword/methods `await`, `Task.WhenAll` and `Task.WhenAny` per se. These approaches include the creation of the (presumably hot) tasks, as you can see in the embedded in the question code. – Theodor Zoulias Mar 13 '20 at 06:23
  • @Nishant if you would like to know what it's like working with TPL Dataflow, you can see [here](https://stackoverflow.com/questions/59565183/improve-performance-of-async-post-using-c-sharp-httpclient/59574072#59574072) a simple example. – Theodor Zoulias Mar 13 '20 at 06:29
2

The simple await will perform each item one after another, essentially synchronously - this would be the slowest.

WhenAll will wait for all of tasks to be done - the runtime will be whatever the longest single task is.

Do not use WaitAll - it is synchronous, just use WhenAll

WhenAny allows you to handle each task as it completes. This in will be faster than WhenAll in some cases, depending on how much processing you have to do after the task.

IMO, unless you need to start post processing immediately when each task complets, WhenAll is the simplest/cleanest approach and would work fine in most scenarios.

Tyler Hundley
  • 819
  • 4
  • 11
  • shouldn't await return the control to caller once it see a blocking call (in this case the REST API call)? Once the control is returned, the control should proceed to execute other await calls? – Nishant Mar 12 '20 at 18:33
  • Is there an internal overhead diff in WhenAll v WhenAny? In whenAll control will return after all is done, while in whenany control may return as soon as one is done. The latter seems to require more internal management. – Nishant Mar 12 '20 at 18:35
  • My assumption is `WhenAny` would end up with more overhead if only slightly, but that would be entirely a guess on my part. The `await` is essentially a stopping point for the code, it will wait until the asynchronous operation is complete before proceeding. – Tyler Hundley Mar 12 '20 at 18:39
  • Depending on how many calls you need ot make, it might be worth looking into [Async Enumerable](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8). It would make sense to leverage `WhanAny` with this. – Tyler Hundley Mar 12 '20 at 18:40
  • 1
    WhenAll does not start tasks, as tasks are usually already running – here FetchBazListFromFoo. It merely wait until the tasks complete. This means that even with the simple await all tasks are running concurrently, because the tasks where created without waiting for one to finish. The only difference between consecutively await each task and using Task.WhenAll is in case an exception occurs, where WhenAll will still wait for everyone to complete, which is not the case for the simple awaits. – ckuri Mar 13 '20 at 05:44
  • If the results are to be used immediately, Microsoft has a helpful blog post that is recommending the WhenAny approach with a while loop: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/start-multiple-async-tasks-and-process-them-as-they-complete – GameSalutes Nov 02 '21 at 15:53