Nishant
Nishant

Reputation: 2619

WhenAny vs WhenAll vs WaitAll vs none, given that results are being used immediately

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;
}

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.

Upvotes: 10

Views: 7354

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43545

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 it will return before all tasks are completed, and it 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 likely 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 it will return after all tasks have been completed, and it 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 likely 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 it will return immediately, and it 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 it will grow exponentially as the number of tasks becomes larger.
Not recommended unless this behavior is exactly what you want (I bet that by now you aren't 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.

A better solution to this problem is to use the specialized API Parallel.ForEachAsync, available from .NET 6 and later. This method parallelizes multiple asynchronous operations, enforces a maximum degree of parallelism which by default is Environment.ProcessorCount, and also supports cancellation and fast completion in case of exceptions. You can find a usage example here. This method does not return the results of the asynchronous operations. You can collect the results as a side effect of the asynchronous operations, as shown here or here.

¹ The order of the Task.WhenAll exceptions changed in .NET 8. For details see this GitHub issue, which is currently unresolved (March 2024).

Upvotes: 10

Tyler Hundley
Tyler Hundley

Reputation: 897

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.

Upvotes: 2

Related Questions