[Source: http://geekswithblogs.net/EltonStoneman]
Using Parallel Extensions you can reduce loop execution time by 95%. Which, if you’re evaluating .NET 4.0 and VS 2010, should be a cinching argument*.
The Parallel Extensions library includes an abundance of features for fine-grained control over concurrent operations, and it also provides the very simple Parallel.ForEach construct, which takes an IEnumerable<> collection and an Action to run over each item, so:
Normal
0
false
false
false
EN-US
X-NONE
X-NONE
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”Table Normal”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-parent:””;
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
mso-para-margin-top:0cm;
mso-para-margin-right:0cm;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0cm;
mso-pagination:widow-orphan;
font-size:11.0pt;
mso-bidi-font-size:10.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:”Times New Roman”;
mso-bidi-theme-font:minor-bidi;}
foreach (var item in GetItems(collectionSize))
{
ServiceCall(item);
}
– becomes:
Normal
0
false
false
false
EN-US
X-NONE
X-NONE
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”Table Normal”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-parent:””;
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
mso-para-margin-top:0cm;
mso-para-margin-right:0cm;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0cm;
mso-pagination:widow-orphan;
font-size:11.0pt;
mso-bidi-font-size:10.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:”Times New Roman”;
mso-bidi-theme-font:minor-bidi;}
Parallel.ForEach(GetItems(collectionSize), x => ServiceCall(x));
The ForEach method partitions the collection and intelligently distributes work amongst the available processors and cores. The workload on the cores is actively monitored while work is being apportioned, so the distribution should be evenly spread whether the actions themselves are an even load or not.
In a simple test rig (available on github here: TPL Sample), I call a stub WCF service in a repeated loop – one version uses a sequential foreach, the other uses Parallel.ForEach. On a dual-core machine, the parallel version completes on average in less than 20% of the time of the sequential version:
But it gets better. Parallel.ForEach is thread-intensive and intended for heavy computional work; for blocking I/O operations like service calls, use of the Task class is preferred. This lets you fire off asynchoronous tasks and wait for the responses without blocking threads:
Normal
0
false
false
false
EN-US
X-NONE
X-NONE
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”Table Normal”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-parent:””;
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
mso-para-margin-top:0cm;
mso-para-margin-right:0cm;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0cm;
mso-pagination:widow-orphan;
font-size:11.0pt;
mso-bidi-font-size:10.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:”Times New Roman”;
mso-bidi-theme-font:minor-bidi;}
var collection = GetItems(collectionSize).ToArray();
var tasks = new Task[collectionSize];
for (int i=0; i<collectionSize; i++)
{
var item = collection[i];
tasks[i] = Task.Factory.StartNew(() => ServiceCall(item));
}
Task.WaitAll(tasks);
So with this approach you can farm out background tasks in a Web service without the risk of starving the ThreadPool so ASP.NET stops processing new requests.
Note that you must use an array of tasks, and you must pull the target item out of the collection before you add it as an action parameter to the task array – don’t try and use List<Task> or () => ServiceCall(collection[i]), you’ll either get an index out of range error, or the tasks will all fire against the first item.
For small collections (calling the service 200 times), the Task method runs in less than 15% of the time for the sequential version:
And for larger collections (3,000 service calls), the improvement is even more impressive – Parallel.ForEach running in 10% of the sequential version’s time, and the Task version in 5%:
Improvements aren’t limited to multi-core boxes either, you’ll still see significant – if less impressive – reduction in processing times on single-core, single-CPU machines.
The usual caveat for performance improvements apply here – you need to test before and after in your own scenarios – and with the parallel extensions, you also need to take your server load into account. Run your tests with no load to get an idea of the maximum improvement available, and run with typical load to see what the real improvement is likely to be.
Microsoft has an excellent introduction to the parallel extensions here: Patterns of Parallel Programming (CSharp) (pdf), which covers the parallel extensions in greater detail.
* – the Reactive Extensions on DevLabs contain a port of the parallel extensions in the System.Threading dll which can be used with .NET 3.5 SP1, but it’s unsupported and doesn’t have the same level of performance as the native TPL in .NET 4.0. If you’re upgrade argument isn’t convincing enough and you’re limited to 3.5 SP1, the ported version still makes for a big improvement over sequential processing.