Using async/await for multiple tasks

336,791

Solution 1

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

Solution 2

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.

Solution 3

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

Solution 4

You can use Task.WhenAll function that you can pass n tasks; Task.WhenAll will return a task which runs to completion when all the tasks that you passed to Task.WhenAll complete. You have to wait asynchronously on Task.WhenAll so that you'll not block your UI thread:

   public async Task DoSomeThing() {
       
       Task[] tasks = new Task[numTasks];
       for(int i = 0; i < numTask; i++)
       {
          tasks[i] = CallSomeAsync();
       }
       await Task.WhenAll(tasks);
       // code that'll execute on UI thread
   }

Solution 5

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.
Share:
336,791
Ben Foster
Author by

Ben Foster

I’m a full-stack software engineer with a passion for software craftsmanship and learning new things. In my 10+ years in the tech industry I have worked with companies in areas such as agriculture, healthcare, recruitment and media. My speciality is web technologies, particularly developing high performance, scalable APIs with rich web-based clients. Running my own startup I have gained experience in product management, marketing and business development. I enjoy working closely with customers and believe a strong focus on customer-experience is the difference between building software and creating products that people love.

Updated on January 20, 2022

Comments

  • Ben Foster
    Ben Foster over 2 years

    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.

  • Rawling
    Rawling over 9 years
    "the above code with WaitAll also blocks the threads" - doesn't it only block one thread, the one that called WaitAll?
  • Mixxiphoid
    Mixxiphoid about 9 years
    @Rawling the documentation states that "Type: System.Threading.Tasks.Task[] An array of Task instances on which to wait.". So, it blocks all threads.
  • musaul
    musaul about 9 years
    @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.
  • tugberk
    tugberk about 9 years
    @musaul I don't believe it's the case but I am not sure. I will check it out.
  • musaul
    musaul about 9 years
    @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.
  • tugberk
    tugberk about 9 years
    @musaul yes, you could be right that only the executing thread is blocked. This is the WaitAll implementation: referencesource.microsoft.com/#mscorlib/system/threading/Tas‌​ks/…
  • Serj Sagan
    Serj Sagan over 8 years
    If you put the time on each of these results, this would be more useful
  • RiaanDP
    RiaanDP over 8 years
    @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.
  • Răzvan Flavius Panda
    Răzvan Flavius Panda about 8 years
    @tugberk See 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.
  • Darius
    Darius almost 5 years
    This await Task.Whenall(..)is the recommended approach: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
  • AnorZaken
    AnorZaken over 4 years
    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.
  • mindplay.dk
    mindplay.dk almost 4 years
    To eliminate speculation, I poked through the source-code. 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.
  • Theodor Zoulias
    Theodor Zoulias almost 4 years
    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.
  • Ygalbel
    Ygalbel almost 4 years
    You right, but I think it's an important remark in this discussion.
  • Andrew Bonsall
    Andrew Bonsall over 2 years
    As of .net 6 you can use Parallel.ForEachAsync https://docs.microsoft.com/en-us/dotnet/api/system.threading‌​.tasks.parallel.fore‌​achasync?view=net-6.‌​0. Like Task.WhenAll this will not block the current thread and is suited for a "ForEach" style loop.
  • Lance U. Matthews
    Lance U. Matthews almost 2 years
    How are this answer and this answer any more "complicated" than what you're doing here? It's awaiting Task.WhenAll() just the same.
  • David Pierson
    David Pierson almost 2 years
    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?
  • JPortillo
    JPortillo almost 2 years
    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.
  • David Pierson
    David Pierson almost 2 years
    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.