PLINQ
PLINQ is the parallel version of LINQ. The objective of parallel
programming is to maximize processor utilization with increased
throughput in a multicore architecture. For a multicore computer, your
application should recognize and scale performance to the number of
available processor cores. As shown earlier, the LINQ query executes
when you iterate over the results, and it executes sequentially. With
PLINQ, the iterations are performed in parallel, as tasks are scheduled
on threads running in the .NET Framework 4 thread pool.
One of the best features of PLINQ is that it’s easy to convert LINQ queries to PLINQ. You can simply add the AsParallel
clause.
from book in books where book.Publisher=="Lucerne Publishing"
orderby book.Title select book;
Now, here’s the same query updated for PLINQ. Note that the only addition to the code is the call to the AsParallel method of the books
collection. This minor change, however, completely alters how the query
is performed. When you iterate over the results, the query is performed
with parallel tasks.
from book in books.AsParallel() where book.Publisher=="Lucerne Publishing"
orderby book.Title select book;
This next tutorial contrasts the productivity of standard LINQ and
PLINQ. You can perform the example query either sequentially or in
parallel. You’ll display information to compare the performance of both
approaches. The task and thread identifiers are also displayed to
highlight the
underlying differences between parallel and sequential execution.
Because the sequential version of the query does not use the Task
Parallel Library (TPL), the task IDs are blank. In addition, the
sequential version will execute on a single thread.
Perform a sequential query and a parallel query on an integer array, and imperatively invoke the Where clause
-
Create a console application for C# in Visual Studio 2010. Add using statements for both the System.Threading and System.Diagnostics namespaces.
-
Above the Main method, create a static method called Normalize that returns a bool. You’ll call this method in the Where clause. In the Normalize method, display the current task and thread identifiers. Use the Thread.SpinWait method to simulate a real-world normalization operation. Return true to select and add the current element to the result collection.
static bool Normalize()
{
Console.WriteLine("Normalizing [Task {0} : Thread {1}]",
Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.SpinWait(int.MaxValue);
return true;
}
-
In Main, create an instance of the Stopwatch class. The Stopwatch is used to calculate the duration of both the sequential and the parallel versions of the PLINQ query. Also, define an integer array that has four elements. This is the array you will query.
Stopwatch sw = new Stopwatch();
var intArray = new [] { 1, 2, 3, 4 };
-
Perform a sequential query on the array by using LINQ to Objects. Call the Where method. Evaluate a lambda expression and call the Normalize method as a parameter. The Where method—and consequently Normalize—will be called for each element of the array. Because the lambda expression returns true
for each element, all elements of the array are included in the
results. On the next line, repeat the query but use PLINQ. Add the AsParallel method. For now, comment out the parallel version of the query. You will initially run only the sequential query.
var result = intArray.Where((index)=>Normalize());
//var result = intArray.AsParallel().Where((index) => Normalize());
-
Start the Stopwatch,
and then iterate the results of the query. Display the results of the
operation. Because of deferred execution, this is when the query
actually executes.
foreach (int item in result)
{
Console.WriteLine("Item={0}", item);
}
-
Call the Stop method on the Stopwatch class and display the duration.
sw.Stop();
Console.WriteLine("Elapse time: {0}: seconds",
sw.ElapsedMilliseconds / 1000);
Here is the entire program.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Performance
{
class Program
{
static bool Normalize()
{
Console.WriteLine("Normalizing [Task {0} : Thread {1}]",
Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.SpinWait(int.MaxValue);
return true;
}
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
var intArray = new [] { 1, 2, 3, 4 };
var result = intArray.Where((index)=>Normalize());
//var result = intArray.AsParallel().Where((index) => Normalize());
sw.Start();
foreach (int item in result)
{
Console.WriteLine("Item={0}", item);
}
sw.Stop();
Console.WriteLine("Elapsed time: {0}: seconds",
sw.ElapsedMilliseconds / 1000);
Console.WriteLine("Press Enter to Continue");
Console.ReadLine();
}
}
}
Build
and run the application. Because the statement containing the PLINQ
query is commented, the code executes only the standard LINQ query.
Each operation is therefore performed sequentially and on the same
thread, which you can see from the output in the console window as
shown in the following image. Because parallel tasks are not used, task
IDs are not displayed. The duration is essentially the sum of running
each of the operations in order.
Now uncomment the statement containing the PLINQ command and comment
out the LINQ query instead. Rerun the application. This time, the
results are entirely different. The Where
method runs in parallel and on different threads, as shown in the
output window in the following image. The PLINQ query leverages the
multicore processor architecture; the results are specific to this
example and the current hardware architecture. In this example, each
iteration of a query operation is a different task. For this reason,
the query runs faster.
The difference in
performance of the two versions is depicted in Processor Utilization of
the Task Manager. The next image shows a screen shot of processor
utilization from the LINQ version of the application. In this example,
average processor utilization is about 12 percent. Most of the
processor computing capability is unused!
However, the PLINQ version of the application is much more
efficient. In the following image, you can see a considerably higher
utilization—more than 50 percent.