When to use Task.WaitAll vs. Task.WhenAll in .NET

Understand the differences between Task.WaitAll and Task.WhenAll methods and when to use which in your application

When to use Task.WaitAll vs. Task.WhenAll in .NET
Thinkstock

The TPL (Task Parallel Library) is one of the most interesting new features added in the recent versions of .NET framework. The Task.WaitAll and Task.WhenAll methods are two important and frequently used methods in the TPL.

The Task.WaitAll blocks the current thread until all other tasks have completed execution. The Task.WhenAll method is used to create a task that will complete if and only if all the other tasks have completed.

So, if you are using Task.WhenAll you will get a task object that isn’t complete. However, it will not block but will allow the program to execute. On the contrary, the Task.WaitAll method call actually blocks and waits for all other tasks to complete.

Essentially, Task.WhenAll will give you a task that isn’t complete, but you can use ContinueWith as soon as the specified tasks have completed their execution. Note that neither Task.WhenAll nor Task.WaitAll will actually run the tasks; i.e., no tasks are started by these methods. Here is how ContinueWith is used with Task.WhenAll: 

Task.WhenAll(taskList).ContinueWith(t => {
  // write your code here
});

As Microsoft’s documentation states, Task.WhenAll “creates a task that will complete when all of the Task objects in an enumerable collection have completed.”

Task.WhenAll vs. Task.WaitAll

Let me explain the difference between these two methods with a simple example. Suppose you have a task that performs some activity with the UI thread — say, some animation needs to be shown in the user interface. Now, if you use Task.WaitAll, the user interface will be blocked and will not be updated until all the related tasks are completed and the block released. However, if you are using Task.WhenAll in the same application, the UI thread will not be blocked and would be updated as usual.

So which of these methods should you use when? Well, you can use WaitAll when the intent is synchronously blocking to get the results. But when you would want to leverage asynchrony, you would want to use the WhenAll variant. You can await Task.WhenAll without having to block the current thread. Hence, you may want to use await with Task.WhenAll inside an async method.

While Task.WaitAll blocks the current thread until all pending tasks are complete, Task.WhenAll returns a task object. Task.WaitAll throws an AggregateException when one or more of the tasks throws an exception. When one or more tasks throw an exception and you await the Task.WhenAll method, it unwraps the AggregateException and returns just the first one.

Avoid using Task.Run in loops

You can use tasks when you would like to execute concurrent activities. If you need a high degree of parallelism, tasks are never a good choice. It is always advisable to avoid using thread pool threads in ASP.Net. Hence, you should refrain from using Task.Run or Task.factory.StartNew in ASP.Net.

Task.Run should always be used for CPU bound code. The Task.Run is not a good choice in ASP.Net applications, or, applications that leverages the ASP.Net runtime since it just offloads the work to a ThreadPool thread. If you are using ASP.Net Web API, the request would already be using a ThreadPool thread. Hence, if you use Task.Run in your ASP.Net Web API application, you are just limiting scalability by offloading the work to another worker thread sans any reason.

Note that there is a disadvantage in using Task.Run in a loop. If you use the Task.Run method inside a loop, multiple tasks would be created -- one for each unit of work or iteration. However, if you use Parallel.ForEach in lieu of using Task.Run inside a loop, a Partitioner gets created to avoid creating more tasks to perform the activity than it is needed. This might improve the performance significantly as you can avoid too many context switches and still leverage multiple cores in your system.

It should be noted that Parallel.ForEach uses Partitioner<T> internally so as to distribute the collection into work items. Incidentally, this distribution doesn't happen for each task in the list of items, rather, it happens as a batch. This lowers the overhead involved and hence improves performance. In other words, if you use Task.Run or Task.Factory.StartNew inside a loop, they would create new tasks explicitly for each iteration in the loop. Parallel.ForEach is much more efficient because it will optimize the execution by distributing the work load across the multiple cores in your system.

Copyright © 2016 IDG Communications, Inc.