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.
For more information, I have an async / await intro on my blog. So additionally, if a method with multiple awaits is called by a caller, the responsibility for finishing every statement of that method is with the caller.
C# Language Async-Await Async/await will only improve performance if it allows the machine to do additional work.
The correct way to await multiple tasks is the Task. WhenAll method: await Task. WhenAll(first, second); . Then you can await them individually to get their results, because you know that all have completed successfully.
You can call multiple asynchronous functions without awaiting them. This will execute them in parallel. While doing so, save the returned promises in variables, and await them at some point either individually or using Promise. all() and process the results.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With