Ok. You are probably new to async and await or maybe you aren’t new but you’ve never deep dived into it. You may not understand some simple truths:
- aync/await does NOT give you parallelism for free.
- Tasks are not necessary parallel. They can be if you code them to be.
- The recommendation “You should always use await” is not really true when you want parallelism, but is still sort of true.
- Task.WhenAll is both parallel and async.
- Task.WaitAll only parallel.
Here is a sample project that will help you learn.
There is more to learn in the comments.
There is more to learn by running this.
Note: I used Visual Studio 2017 and compiled with .Net 7.1, which required that I go to the project properties | Build | Advanced | Language Version and set the language to C# 7.1 or C# latest minor version.
using System; using System.Diagnostics; using System.Threading.Tasks; namespace MultipleAwaitsExample { class Program { static async Task Main(string[] args) { Console.WriteLine("Running with await"); await RunTasksAwait(); Console.WriteLine("Running with Task.WaitAll()"); await RunTasksWaitAll(); Console.WriteLine("Running with Task.WhenAll()"); await RunTasksWhenAll(); Console.WriteLine("Running with Task.Run()"); await RunTasksWithTaskRun(); Console.WriteLine("Running with Parallel"); RunTasksWithParallel(); } /// <summary> /// Pros: It works /// Cons: The tasks are NOT run in parallel. /// Code after the await is not run while the await is awaited /// **If you want parallelism, this isn't even an option.** /// Slowest. Because of no parallelism. /// </summary> public static async Task RunTasksAwait() { var group = "await"; Stopwatch watcher = new Stopwatch(); watcher.Start(); await MyTaskAsync(1, 500, group); await MyTaskAsync(2, 300, group); await MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WaitAll behaves quite differently from WhenAll /// Pros: It works /// The tasks run in parallel /// Cons: It isn't clear whether the code is parallel here, but it is. /// It isn't clear whether the code is async here, but it is NOT. /// There is a Visual Studio usage warning. You can remove async to get rid of it because it isn't an Async method. /// The return value is wrapped the Result property of the task /// Breaks Aync end-to-end /// Note: I can't foresee usecase where WaitAll would be preferred over WhenAll. /// </summary> public static async Task RunTasksWaitAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); Task.WaitAll(task1, task2, task3); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WhenAll gives you the best of all worlds. The code is both parallel and async. /// Pros: It works /// The tasks run in parallel /// Code after the tasks run while the task is running /// Doesn't break end-to-end async /// Cons: It isn't clear you are doing parallelism here, but you are. /// There is a Visual Studio usage warning /// The return value is wrapped the Result property of the task /// </summary> public static async Task RunTasksWhenAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); // You can't use await if you want parallelism var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); await Task.WhenAll(task1, task2, task3); // But now you are calling await, so you are sort of still awaiting watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// The tasks run in parrallel /// Code can run immediately after the tasks but before the tasks complete /// Allows for running non-async code asynchonously /// Cons: It isn't clear whether the code is doing parallelism here. It isn't. /// The lambda syntax affects readability /// Breaks Aync end-to-end /// </summary> public static async Task RunTasksWithTaskRun() { var group = "Task.Run()"; Stopwatch watcher = new Stopwatch(); watcher.Start(); await Task.Run(() => MyTask(1, 500, group)); await Task.Run(() => MyTask(2, 300, group)); await Task.Run(() => MyTask(3, 100, group)); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// It is clear in the code you want to run these tasks in parallel. /// Code can run immediately after the tasks but before the tasks complete /// Fastest /// Cons: There is no async or await. /// Breaks Aync end-to-end. You can workaround this by wrapping Parallel.Invoke in a Task.Run method. See commented code. /// </summary> public /* async */ static void RunTasksWithParallel() { var group = "Parallel"; Stopwatch watcher = new Stopwatch(); watcher.Start(); //await Task.Run(() => Parallel.Invoke( () => MyTask(1, 500, group), () => MyTask(2, 300, group), () => MyTask(3, 100, group), () => Console.WriteLine("Code immediately after tasks.") ); //); watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } public static async Task MyTaskAsync(int i, int milliseconds, string group) { await Task.Delay(milliseconds); Console.WriteLine($"{group}: {i}"); } public static void MyTask(int i, int milliseconds, string group) { var task = Task.Delay(milliseconds); task.Wait(); Console.WriteLine($"{group}: {i}"); } } }
And here is the same example but this time with some return values.
using System; using System.Diagnostics; using System.Threading.Tasks; namespace MultipleAwaitsExample { class Program1 { static async Task Main(string[] args) { Console.WriteLine("Running with await"); await RunTasksAwait(); Console.WriteLine("Running with Task.WaitAll()"); await RunTasksWaitAll(); Console.WriteLine("Running with Task.WhenAll()"); await RunTasksWhenAll(); Console.WriteLine("Running with Task.Run()"); await RunTasksWithTaskRun(); Console.WriteLine("Running with Parallel"); RunTasksWithParallel(); } /// <summary> /// Pros: It works /// Cons: The tasks are NOT run in parallel. /// Code after the await is not run while the await is awaited /// **If you want parallelism, this isn't even an option.** /// Slowest. Because of no parallelism. /// </summary> public static async Task RunTasksAwait() { var group = "await"; Stopwatch watcher = new Stopwatch(); watcher.Start(); // You just asign the return variables as normal. int result1 = await MyTaskAsync(1, 500, group); int result2 = await MyTaskAsync(2, 300, group); int result3 = await MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); // You now have access to the return objects directly. Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WaitAll behaves quite differently from WhenAll /// Pros: It works /// The tasks run in parallel /// Cons: It isn't clear whether the code is parallel here, but it is. /// It isn't clear whether the code is async here, but it is NOT. /// There is a Visual Studio usage warning. You can remove async to get rid of it because it isn't an Async method. /// The return value is wrapped the Result property of the task /// Breaks Aync end-to-end /// Note: I can't foresee usecase where WaitAll would be preferred over WhenAll. /// </summary> public static async Task RunTasksWaitAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); Task.WaitAll(task1, task2, task3); watcher.Stop(); // You now have access to the return object using the Result property. int result1 = task1.Result; int result2 = task2.Result; int result3 = task3.Result; Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// WhenAll gives you the best of all worlds. The code is both parallel and async. /// Pros: It works /// The tasks run in parallel /// Code after the tasks run while the task is running /// Doesn't break end-to-end async /// Cons: It isn't clear you are doing parallelism here, but you are. /// There is a Visual Studio usage warning /// The return value is wrapped the Result property of the task /// </summary> public static async Task RunTasksWhenAll() { var group = "WaitAll"; Stopwatch watcher = new Stopwatch(); watcher.Start(); var task1 = MyTaskAsync(1, 500, group); // You can't use await if you want parallelism var task2 = MyTaskAsync(2, 300, group); var task3 = MyTaskAsync(3, 100, group); Console.WriteLine("Code immediately after tasks."); await Task.WhenAll(task1, task2, task3); // But now you are calling await, so you are sort of still awaiting watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// The tasks run in parrallel /// Code can run immediately after the tasks but before the tasks complete /// Allows for running non-async code asynchonously /// Cons: It isn't clear whether the code is doing parallelism here. It isn't. /// The lambda syntax affects readability /// Breaks Aync end-to-end /// </summary> public static async Task RunTasksWithTaskRun() { var group = "Task.Run()"; Stopwatch watcher = new Stopwatch(); watcher.Start(); int result1 = await Task.Run(() => MyTask(1, 500, group)); int result2 = await Task.Run(() => MyTask(2, 300, group)); int result3 = await Task.Run(() => MyTask(3, 100, group)); Console.WriteLine("Code immediately after tasks."); watcher.Stop(); // You now have access to the return objects directly. Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } /// <summary> /// Pros: It works /// It is clear in the code you want to run these tasks in parallel. /// Code can run immediately after the tasks but before the tasks complete /// Fastest /// Cons: There is no async or await. /// Breaks Aync end-to-end. You can workaround this by wrapping Parallel.Invoke in a Task.Run method. See commented code. /// </summary> public /* async */ static void RunTasksWithParallel() { var group = "Parallel"; Stopwatch watcher = new Stopwatch(); watcher.Start(); // You have to declare your return objects before hand. //await Task.Run(() => int result1, result2, result3; Parallel.Invoke( () => result1 = MyTask(1, 500, group), () => result2 = MyTask(2, 300, group), () => result3 = MyTask(3, 100, group), () => Console.WriteLine("Code immediately after tasks.") ); //); // You now have access to the return objects directly. watcher.Stop(); Console.WriteLine($"{group} runtime: {watcher.ElapsedMilliseconds}"); } public static async Task<int> MyTaskAsync(int i, int milliseconds, string group) { await Task.Delay(milliseconds); Console.WriteLine($"{group}: {i}"); return i; } public static int MyTask(int i, int milliseconds, string group) { var task = Task.Delay(milliseconds); task.Wait(); Console.WriteLine($"{group}: {i}"); return i; } } }