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...