On a recent project, we needed to pull in a significant amount of data from the database, all from various webservices. This is a classic case for concurrent processing. Two techniques are: Task.WhenAll()
and Parellel.Invoke()
First (wrong) attempt: Using Task.WaitAll:
For my first attempt at this, I tried the following:
public ResultData GetResults(int id) { var result = new ResultData(); var group1DataTask = getGroup1DataAsync(id); var group2DataTask = getGroup2DataAsync(id); var group3DataTask = getGroup3DataAsync(id); // WARNING, DON'T DO THIS: Task.WaitAll(group1DataTask , getGroup2DataTask , getGroup3DataTask ); result.Group1 = group1DataTask.Result; result.Group2 = group2DataTask.Result; result.Group3 = group3DataTask.Result; return resultData; }
Task.WaitAll seemed like a good option. The problem here is that both .WaitAll and .Result are blocking operations. In this implementation, it was leading to deadlocks. Note that I have found cases where Task.WaitAll
does work properly though. See here.
A better approach: Using Task.WhenAll:
First, convert this function to async, then use await Task.WhenAll
.
public async ResultData GetResults(int id) { var result = new ResultData(); var group1DataTask = getGroup1DataAsync(id); var group2DataTask = getGroup2DataAsync(id); var group3DataTask = getGroup3DataAsync(id); // A better solution: await Task.WhenAll(group1DataTask , getGroup2DataTask , getGroup3DataTask ); result.Group1 = group1DataTask.Result; result.Group2 = group2DataTask.Result; result.Group3 = group3DataTask.Result; return resultData; }
This approach does not use .WaitAll
and .Result
calls. This is a safer approach on UI and ASP.NET applications. Notice that 'await' can now be added to the WhenAll call, and after the call I used await group1DataTask
instead of group1DataTask.Result
.
Remember: await
is asynchronous while .Wait, .WaitAll & .Result
block the current thread.
WaitAll vs WhenAll
The primary differences are that WaitAll
is a function that returns void, and blocks the current thread until all are complete. On a UI or ASP.NET, these can lead to deadlocks. WhenAll
returns a task that can be awaited. This has a few advantages in that first off, you can keep the UI from blocking by doing the following:
await Task.WhenAll(...);
Plus, you can do anything else that can be done with Task objects, such as:
Task.WhenAll(...).ContinueWith(t => ... );
How about neither??
It's worth noting that the following does produce concurrency:
public ResultData GetResults(int id) { var result = new ResultData(); var group1DataTask = getGroup1DataAsync(id); var group2DataTask = getGroup2DataAsync(id); var group3DataTask = getGroup3DataAsync(id); result.Group1 = await group1DataTask; result.Group2 = await group2DataTask; result.Group3 = await group3DataTask; return resultData; }
Each call does get kicked off in a concurrent fashion, and the return line isn't hit until all three awaits complete, making this a simple solution. The biggest disadvantages to this approach are:
- It doesn't allow for partial success. Task.WhenAll will wait for all to complete, even if one throws an exception.
- It doesn't let you log all thrown exceptions, only the first one
Using Parallel.Invoke()
Parallel.Invoke is another means of performing concurrency. Here's a simple example of using Parallel.Invoke:
public ResultData GetResults(int id) { var result = new ResultData(); Parallel.Invoke( () => result.GroupA = getGroupAData(id), () => result.GroupB = getGroupBData(id), () => result.GroupC = getGroupCData(id) ); return resultData; }
This is mentioned only to introduce another option for making parallel calls. Your mileage on this may vary based on your application. A full discussion on Parallel.Invoke will have to wait for another day...