Andrzej Gis
Andrzej Gis

Reputation: 14306

What's the diference between Task.WhenAll() and foreach(var task in tasks)

After a few hours of struggle I found a bug in my app. I considered the 2 functions below to have identical behavior, but it turned out they don't.

Can anyone tell me what's really going on under the hood, and why they behave in a different way?

public async Task MyFunction1(IEnumerable<Task> tasks){
    await Task.WhenAll(tasks);

    Console.WriteLine("all done"); // happens AFTER all tasks are finished
}

public async Task MyFunction2(IEnumerable<Task> tasks){
    foreach(var task in tasks){
        await task;
    }

    Console.WriteLine("all done"); // happens BEFORE all tasks are finished
}

Upvotes: 3

Views: 1849

Answers (2)

Saeb Amini
Saeb Amini

Reputation: 24400

Likely the most important functional difference is that Task.WhenAll can introduce concurrency when your tasks perform truly asynchronous operations, for example, IO. This may or may not be what you want depending on your situation.

For example, if your tasks are querying the database using the same EF DbContext, the next query would fire as soon as the first one is "in flight" which causes EF to blow up as it doesn't support multiple simultaneous queries using the same context.

That's because you're not awaiting each asynchronous operation individually. You're awaiting a task that represents the completion of all of those asynchronous operations. They can also be completed in any order.

However when you await each one individually in a foreach, you only fire the next task when the current one completes, preventing concurrency and ensuring serial execution.

A simple example demonstrating this behavior:

async Task Main()
{
    var tasks = new []{1, 2, 3, 4, 5}.Select(i => OperationAsync(i));

    foreach(var t in tasks)
    {
        await t;
    }

    await Task.WhenAll(tasks);
}

static Random _rand = new Random();
public async Task OperationAsync(int number)
{
    // simulate an asynchronous operation
    // taking anywhere between 100 to 3000 milliseconds
    await Task.Delay(_rand.Next(100, 3000));
    Console.WriteLine(number);
}

You'll see that no matter how long OperationAsync takes, with foreach you always get 1, 2, 3, 4, 5 printed. But with Task.WhenAll they are executed concurrently and printed in their completion order.

Upvotes: 1

Servy
Servy

Reputation: 203830

They'll function identically if all tasks complete successfully.

If you use WhenAll and any items fail, it still won't be completed until all of the items are finished, and it'll represent an AggregatException that wraps all errors from all tasks.

If you await each one then it'll complete as soon as it hits any item that fails, and it'll represent an exception for that one error, not any others.


The two also differ in that WhenAll will materialize the entire IEnumerable right at the start, before adding any continuations to other items. If the IEnumerable represents a collection of already existing and started tasks, then this isn't relevant, but if the act of iterating the enumerable creates and/or starts tasks, then materializing the sequence at the start would run them all in parallel, and awaiting each before fetching the next task would execute them sequentially. Below is a IEnumerable you could pass in that would behave as I've described here:

public static IEnumerable<Task> TaskGeneratorSequence()
{
    for(int i = 0; i < 10; i++)
        yield return Task.Delay(TimeSpan.FromSeconds(2);
}

Upvotes: 8

Related Questions