525

I'm using an API client that is completely asynchrounous, that is, each operation either returns Task or Task<T>, e.g:

static async Task DoSomething(int siteId, int postId, IBlogClient client)
{
    await client.DeletePost(siteId, postId); // call API client
    Console.WriteLine("Deleted post {0}.", siteId);
}

Using the C# 5 async/await operators, what is the correct/most efficient way to start multiple tasks and wait for them all to complete:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

or:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

Since the API client is using HttpClient internally, I would expect this to issue 5 HTTP requests immediately, writing to the console as each one completes.

tugberk
  • 57,477
  • 67
  • 243
  • 335
Ben Foster
  • 34,340
  • 40
  • 176
  • 285
  • 2
    And what is the problem ? – Serg Shevchenko Sep 20 '18 at 16:25
  • 5
    @SergShevchenko The problem is that his Parallel.ForEach is done incorrecly (see answers) - he's asking if his attempts to run async code in parallel is correct, offering two solution attempts, and if one is better than the other (and presumably why so). – AnorZaken Jan 17 '20 at 14:34
  • Since no one has mentioned worth noting the MSDN docs breakfast analogy breaking it all down: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ – markmnl Jan 24 '23 at 13:26

8 Answers8

714
int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

Although you run the operations in parallel with the above code, this code blocks each thread that each operation runs on. For example, if the network call takes 2 seconds, each thread hangs for 2 seconds w/o doing anything but waiting.

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

On the other hand, the above code with WaitAll also blocks the threads and your threads won't be free to process any other work till the operation ends.

Recommended Approach

I would prefer WhenAll which will perform your operations asynchronously in Parallel.

public async Task DoWork() {

    int[] ids = new[] { 1, 2, 3, 4, 5 };
    await Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

In fact, in the above case, you don't even need to await, you can just directly return from the method as you don't have any continuations:

public Task DoWork() 
{
    int[] ids = new[] { 1, 2, 3, 4, 5 };
    return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

To back this up, here is a detailed blog post going through all the alternatives and their advantages/disadvantages: How and Where Concurrent Asynchronous I/O with ASP.NET Web API

tugberk
  • 57,477
  • 67
  • 243
  • 335
  • 42
    "the above code with `WaitAll` also blocks the threads" - doesn't it only block *one* thread, the one that called `WaitAll`? – Rawling Jan 09 '15 at 11:38
  • 6
    @Rawling the [documentation](https://msdn.microsoft.com/en-us/library/dd270695(v=vs.110).aspx) states that "Type: System.Threading.Tasks.Task[] An array of Task instances on which to wait.". So, it blocks all threads. – Mixxiphoid Feb 20 '15 at 07:56
  • 46
    @Mixxiphoid: The bit you quoted does not mean that it blocks all threads. It blocks only the calling thread while the supplied tasks are running. How those tasks are actually run, depends on the scheduler. Typically after each task completes, the thread it was running on would get returned to the pool. Each thread would not remain blocked until others are complete. – musaul Apr 01 '15 at 13:58
  • @musaul I don't believe it's the case but I am not sure. I will check it out. – tugberk Apr 02 '15 at 10:11
  • 4
    @tugberk, The way I understand it, the only difference between the the "classic" Task methods and the Async counterparts is how they interact with threads between when a task starts running and it finishes running. The classic method under a default scheduler will hog a thread during that period (even if it is "sleeping"), while the async ones will not. No difference outside of that period, i.e. the task is schedule but not started, and when it has completed but it's caller is still waiting. – musaul Apr 02 '15 at 11:02
  • @musaul yes, you could be right that only the executing thread is blocked. This is the `WaitAll` implementation: http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,72b6b3fa5eb35695 – tugberk Apr 02 '15 at 12:12
  • 5
    @tugberk See http://stackoverflow.com/a/6123432/750216 the difference is in whether the calling thread is blocked or not, rest is the same. You might want to edit the answer to clarify. – Răzvan Flavius Panda Feb 24 '16 at 10:54
  • This `await Task.Whenall(..)`is the recommended approach: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/#await-tasks-efficiently – Darius Jun 07 '19 at 09:06
  • 2
    To eliminate speculation, I poked through the [source-code](https://github.com/microsoft/referencesource/blob/master/mscorlib/system/threading/Tasks/Task.cs#L4893). Basically, the implementation of `Task.WaitAll()` implements timeouts and cancellation - so if you don't need either of those features, probably prefer `await Task.WhenAll()`, which is a lot simpler, and arguably the `async` keyword makes it a bit easier to read/understand in the context of an `async` method than a blocking method-call. – mindplay.dk Jun 02 '20 at 07:51
  • 1
    As of .net 6 you can use `Parallel.ForEachAsync` [https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync?view=net-6.0](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync?view=net-6.0). Like `Task.WhenAll` this will not block the current thread and is suited for a "ForEach" style loop. – Andrew Bonsall Jan 27 '22 at 22:10
66

I was curious to see the results of the methods provided in the question as well as the accepted answer, so I put it to the test.

Here's the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        class Worker
        {
            public int Id;
            public int SleepTimeout;

            public async Task DoWork(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart-testStart).TotalSeconds.ToString("F2"));
                await Task.Run(() => Thread.Sleep(SleepTimeout));
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd-workerStart).TotalSeconds.ToString("F2"), (workerEnd-testStart).TotalSeconds.ToString("F2"));
            }
        }

        static void Main(string[] args)
        {
            var workers = new List<Worker>
            {
                new Worker { Id = 1, SleepTimeout = 1000 },
                new Worker { Id = 2, SleepTimeout = 2000 },
                new Worker { Id = 3, SleepTimeout = 3000 },
                new Worker { Id = 4, SleepTimeout = 4000 },
                new Worker { Id = 5, SleepTimeout = 5000 },
            };

            var startTime = DateTime.Now;
            Console.WriteLine("Starting test: Parallel.ForEach...");
            PerformTest_ParallelForEach(workers, startTime);
            var endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WaitAll...");
            PerformTest_TaskWaitAll(workers, startTime);
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WhenAll...");
            var task = PerformTest_TaskWhenAll(workers, startTime);
            task.Wait();
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            Console.ReadKey();
        }

        static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
        {
            Parallel.ForEach(workers, worker => worker.DoWork(testStart).Wait());
        }

        static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
        {
            Task.WaitAll(workers.Select(worker => worker.DoWork(testStart)).ToArray());
        }

        static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
        {
            return Task.WhenAll(workers.Select(worker => worker.DoWork(testStart)));
        }
    }
}

And the resulting output:

Starting test: Parallel.ForEach...
Worker 1 started on thread 1, beginning 0.21 seconds after test start.
Worker 4 started on thread 5, beginning 0.21 seconds after test start.
Worker 2 started on thread 3, beginning 0.21 seconds after test start.
Worker 5 started on thread 6, beginning 0.21 seconds after test start.
Worker 3 started on thread 4, beginning 0.21 seconds after test start.
Worker 1 stopped; the worker took 1.90 seconds, and it finished 2.11 seconds after the test start.
Worker 2 stopped; the worker took 3.89 seconds, and it finished 4.10 seconds after the test start.
Worker 3 stopped; the worker took 5.89 seconds, and it finished 6.10 seconds after the test start.
Worker 4 stopped; the worker took 5.90 seconds, and it finished 6.11 seconds after the test start.
Worker 5 stopped; the worker took 8.89 seconds, and it finished 9.10 seconds after the test start.
Test finished after 9.10 seconds.

Starting test: Task.WaitAll...
Worker 1 started on thread 1, beginning 0.01 seconds after test start.
Worker 2 started on thread 1, beginning 0.01 seconds after test start.
Worker 3 started on thread 1, beginning 0.01 seconds after test start.
Worker 4 started on thread 1, beginning 0.01 seconds after test start.
Worker 5 started on thread 1, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.

Starting test: Task.WhenAll...
Worker 1 started on thread 1, beginning 0.00 seconds after test start.
Worker 2 started on thread 1, beginning 0.00 seconds after test start.
Worker 3 started on thread 1, beginning 0.00 seconds after test start.
Worker 4 started on thread 1, beginning 0.00 seconds after test start.
Worker 5 started on thread 1, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.00 seconds after the test start.
Test finished after 5.00 seconds.
Bogdan Stăncescu
  • 5,320
  • 3
  • 24
  • 25
RiaanDP
  • 1,212
  • 12
  • 18
  • 4
    If you put the time on each of these results, this would be more useful – Serj Sagan Oct 01 '15 at 01:41
  • 12
    @SerjSagan my initial idea was just to verify that the workers are being started concurrently in each case, but I've added time stamps to improve the clarity of the test. Thanks for the suggestion. – RiaanDP Oct 01 '15 at 05:44
  • Thank you for the test. However it feels a bit odd that you are running thread.sleep on a thread separate from the "worker thread". Not that it matters in this case, but wouldnt it make more sense to Task.Run the worker threads if we are simulating computational work, or just Task.Delay instead of sleep if we are simulating i/o? Just checking what your thoughts would be on that. – AnorZaken Jan 17 '20 at 14:26
32

You can use the Task.WhenAll function, to which you can pass any number of tasks. The Task.WhenAll returns a new task that will complete when all the tasks have completed. Be sure to wait asynchronously on Task.WhenAll, to avoid blocking your UI thread:

public async Task DoSomethingAsync() {
    Task[] tasks = new Task[numTasks];
    for (int i = 0; i < numTasks; i++)
    {
        tasks[i] = DoChildTaskAsync();
    }
    await Task.WhenAll(tasks);
    // Code here will execute on UI thread
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Ahmed Wasim
  • 351
  • 3
  • 5
26

Since the API you're calling is async, the Parallel.ForEach version doesn't make much sense. You shouldnt use .Wait in the WaitAll version since that would lose the parallelism Another alternative if the caller is async is using Task.WhenAll after doing Select and ToArray to generate the array of tasks. A second alternative is using Rx 2.0

Liam
  • 27,717
  • 28
  • 128
  • 190
James Manning
  • 13,429
  • 2
  • 40
  • 64
13

Parallel.ForEach requires a list of user-defined workers and a non-async Action to perform with each worker.

Task.WaitAll and Task.WhenAll require a List<Task>, which are by definition asynchronous.

I found RiaanDP's response very useful to understand the difference, but it needs a correction for Parallel.ForEach. Not enough reputation to respond to his comment, thus my own response.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        class Worker
        {
            public int Id;
            public int SleepTimeout;

            public void DoWork(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
                Thread.Sleep(SleepTimeout);
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
            }

            public async Task DoWorkAsync(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
                await Task.Run(() => Thread.Sleep(SleepTimeout));
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
            }
        }

        static void Main(string[] args)
        {
            var workers = new List<Worker>
            {
                new Worker { Id = 1, SleepTimeout = 1000 },
                new Worker { Id = 2, SleepTimeout = 2000 },
                new Worker { Id = 3, SleepTimeout = 3000 },
                new Worker { Id = 4, SleepTimeout = 4000 },
                new Worker { Id = 5, SleepTimeout = 5000 },
            };

            var startTime = DateTime.Now;
            Console.WriteLine("Starting test: Parallel.ForEach...");
            PerformTest_ParallelForEach(workers, startTime);
            var endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WaitAll...");
            PerformTest_TaskWaitAll(workers, startTime);
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WhenAll...");
            var task = PerformTest_TaskWhenAll(workers, startTime);
            task.Wait();
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            Console.ReadKey();
        }

        static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
        {
            Parallel.ForEach(workers, worker => worker.DoWork(testStart));
        }

        static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
        {
            Task.WaitAll(workers.Select(worker => worker.DoWorkAsync(testStart)).ToArray());
        }

        static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
        {
            return Task.WhenAll(workers.Select(worker => worker.DoWorkAsync(testStart)));
        }
    }
}

The resulting output is below. Execution times are comparable. I ran this test while my computer was doing the weekly anti virus scan. Changing the order of the tests did change the execution times on them.

Starting test: Parallel.ForEach...
Worker 1 started on thread 9, beginning 0.02 seconds after test start.
Worker 2 started on thread 10, beginning 0.02 seconds after test start.
Worker 3 started on thread 11, beginning 0.02 seconds after test start.
Worker 4 started on thread 13, beginning 0.03 seconds after test start.
Worker 5 started on thread 14, beginning 0.03 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.02 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.02 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.03 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.03 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.03 seconds after the test start.
Test finished after 5.03 seconds.

Starting test: Task.WaitAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.

Starting test: Task.WhenAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.
JPortillo
  • 543
  • 3
  • 9
  • That is most interesting. Removing the ".Wait()" from the Parallel workers, makes them run at the same speed as the others. And equally, the PerformTest_ParallelForEach doesn't return too soon - it still waits for workers to complete. Not sure why, when Parallel uses 5 separate threads, they do not all run side-by-side and complete in one second. What am I missing? – David Pierson Jul 06 '22 at 10:40
  • 1
    The difference between previous responses and mine is that I pass different worker types to the iterators. I pass `DoWork` (non-`async` `Action`) to `Parallel.ForEach` and `DoWorkAsync` (`async Task`) to `Task.WaitAll` and `Task.WhenAll`. `Parallel.ForEach` requires a `Task`. Adding `.Wait()` to `DoWorkAsync` makes it a `Task`, but this prevents concurrency, which is not what we want. – JPortillo Jul 07 '22 at 00:29
  • Cheers that makes sense. My bad, they are indeed running side-by-side in the Parallel.ForEach but they have different SleepTimeout values. Missed that. – David Pierson Jul 08 '22 at 01:36
6

The question is 10 years old and OP was asking about C# 5.

As of today, there is one more option: Parallel.ForEachAsync method that was introduced in .NET 6.

Here is an example based on the OP's code:

int[] ids = new[] { 1, 2, 3, 4, 5 };
await Parallel.ForEachAsync(ids, async (i,token) => await DoSomething(1, i, blogClient));

This is completely asynchronous and doesn't block any threads.

Additionally, it might be better than Task.WaitAll and Task.WhenAll approaches because they don't limit the number of threads running in parallel. So if you have a huge array it can eat up all your RAM. Parallel.ForEachAsync allows you to specify parallelism degree like so:

var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };

await Parallel.ForEachAsync(ids, options, async (i,token) => await DoSomething(1, i, blogClient));

This way you have only 4 threads running in parallel.

AlexPavlov
  • 312
  • 4
  • 7
5

All the answers are for running the same function.

The following code works for calling different functions. Just put your regular Task.Run() inside an array and call with Task.WhenAll():

await Task.WhenAll(new Task[] { 
    Task.Run(() => Func1(args)),
    Task.Run(() => Func2(args))
});
Feng Jiang
  • 1,776
  • 19
  • 25
  • How are [this answer](https://stackoverflow.com/a/12338717/150605) and [this answer](https://stackoverflow.com/a/48140851/150605) any more "complicated" than what you're doing here? It's `await`ing `Task.WhenAll()` just the same. – Lance U. Matthews Jun 30 '22 at 03:26
1

I just want to add to all great answers above, that if you write a library it's a good practice to use ConfigureAwait(false) and get better performance, as said here.

So this snippet seems to be better:

 public static async Task DoWork() 
 {
     int[] ids = new[] { 1, 2, 3, 4, 5 };
     await Task.WhenAll(ids.Select(i => DoSomething(1, i))).ConfigureAwait(false);
 }

A full fiddle link here.

Ygalbel
  • 5,214
  • 1
  • 24
  • 32
  • 1
    This is correct, but I doubt that the OP is writing a library. It looks more probable that they are writing application code, where `ConfigureAwait` just clutters the code and gets in the way, offering practically nothing (performance-wise) in return. – Theodor Zoulias Jul 12 '20 at 07:57
  • 2
    You right, but I think it's an important remark in this discussion. – Ygalbel Jul 12 '20 at 08:02