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;
}
}
}