PLINQ

Language-Integrated Query (LINQ) was introduced in .NET Framework 3.5. It allows us to query in-memory collections such as List<T>. You will learn more about LINQ in Chapter 15, Using LINQ Queries. However, if you want to find out more sooner, more information can be found at https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/index.

PLINQ is the parallel implementation of the LINQ pattern. They resemble LINQ queries and operate on any in-memory collections but differ in terms of execution. PLINQ uses all the available processors in the system. However, the processors are limited to 64  bits. This is achieved by partitioning the data source into smaller tasks and executing each task on separate worker threads on multiple processors. 

Most of the standard query operators are implemented in the System.Linq.ParallelEnumerable class. The following table lists the various parallel execution-specific methods:

AsParallel When you want a system to perform parallel execution on an enumerable collection, the AsParallel instruction can be provided to the system.
AsSequential Instructing the system to run sequentially can be achieved by using AsSequential.
AsOrdered To maintain the order on the result set, use AsOrdered.
AsUnordered To not maintain the order on the result set, use AsUnordered.
WithCancellation A cancellation token carries the user's request to cancel the execution. This has to be monitored so that execution can be canceled at any time.
WithDegreeofParallelism Controls the number of processors to be used in a parallel query.
WithMergeOptions Provides options so that we can merge results to the parent task/thread/result set.
WithExecutionMode Forces the runtime to use either parallel or sequential modes.
ForAll Allows results to be processed in parallel by not merging to the parent thread.
Aggregate

A unique PLINQ overload to enable intermediate aggregation over thread-local partitions. Also allows us to merge the final aggregation to combine the results of all partitions.

Let's try to use some of these methods so that we can understand them in more detail. The AsParallel extension method binds query operators such as where and select to the parallelEnumerable implementation. By simply specifying AsParallel, we tell the compiler to execute the query in parallel:

public static void PrintEvenNumbers()
{
var numbers = Enumerable.Range(1, 20);
var pResult = numbers.AsParallel().Where(i => i % 2 == 0).ToArray();

foreach (int e in pResult)
{
Console.WriteLine(e);
}

}

When executed, the preceding code block identifies all even numbers and prints them on the screen:

As you can see, the even numbers weren't printed in order. One thing to remember regarding parallel processing is that it does not guarantee any particular order. Try executing the code block multiple times and observe the output. It will differ each time since it is based on the number of processors that are available at the time of execution. 

By using the AsOrdered operator, the code block accepts a range of numbers between 1 and 20. However, using AsOrdered will order the numbers:

public static void PrintEvenNumbersOrdered()
{
var numbers = Enumerable.Range(1, 20);
var pResult = numbers.AsParallel().AsOrdered()
.Where(i => i % 2 == 0).ToArray();

foreach (int e in pResult)
{
Console.WriteLine(e);
}

}

This example shows how we can maintain the order of the result set when using Parallel:

2
4
6
8
10
12
14
16
18
20
Press any key to exit.

When you execute a code block using PLINQ, the runtime analyzes whether it is safe to parallelize the query. If it is, it partitions the query into tasks and then runs them concurrently. If it isn't safe to parallelize the query, it executes the query in a sequential pattern. In terms of performance, using a sequential algorithm is better than using a parallel algorithm, so by default, PLINQ selects the sequential algorithm. Using ExecutionMode will allow us to instruct PLINQ to select the parallel algorithm.

The following code block shows how we can use ExecutionMode:

public static void PrintEvenNumbersExecutionMode()
{
var numbers = Enumerable.Range(1, 20);
var pResult = numbers.AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.Where(i => i % 2 == 0).ToArray();

foreach (int e in pResult)
{
Console.WriteLine(e);
}
}

As we mentioned previously, PLINQ uses all the processors by default. However, by using the WihtDegreeofParallelism method, we can control the number of processors to be used:

public static void PrintEvenNumbersDegreeOfParallel()
{
var numbers = Enumerable.Range(1, 20);
var pResult = numbers.AsParallel().WithDegreeOfParallelism(3)
.Where(i => i % 2 == 0).ToArray();

foreach (int e in pResult)
{
Console.WriteLine(e);
}

}

Execute the preceding code block by changing the number of processors and observe the output. In the first scenario, we left the system to use the available cores/processors, but in the second one, we instructed the system to use three cores. You will see that the difference in performance is based on your system's configuration.

PLINQ also comes with a method called AsSequential. This is used to instruct PLINQ to execute queries sequentially until AsParallel is called.

forEach can be used to iterate through all the results of a PLINQ query and merges the output from each task to the parent thread. In the preceding examples, we used forEach to display even numbers.

forEach can be used to preserve the order of the PLINQ query results. So, when order preservation is not required and we want to achieve faster query execution, we can use the ForAll method. ForAll does not perform the final merge step; instead, it parallelizes the processing of results. The following code block is using ForAll to print output to the screen:

public static void PrintEvenNumbersForAll()
{
var numbers = Enumerable.Range(1, 20);
var pResult = numbers.AsParallel().Where(i => i % 2 == 0);

pResult.ForAll(e => Console.WriteLine(e));
}

In this scenario, the I/O is being used by multiple tasks, so the numbers will appear in a random order:

When PLINQ executes in multiple threads, as the code runs, the application logic may fail in one or more threads. PLINQ uses the Aggregate exception to encapsulate all the exceptions that are thrown by a query and sends them back to the calling thread. When doing this, you need to have one try..catch block on the calling thread. When you get the results from the query, the developer can traverse through all the exceptions encapsulated in AggregatedException:

public static void PrintEvenNumbersExceptions()
{
var numbers = Enumerable.Range(1, 20);
try
{
var pResult = numbers.AsParallel().Where(i => IsDivisibleBy2(i));

pResult.ForAll(e => Console.WriteLine(e));
}
catch (AggregateException ex)
{
Console.WriteLine("There were {0} exceptions", ex.InnerExceptions.Count);
foreach (Exception e in ex.InnerExceptions)
{
Console.WriteLine("Exception Type: {0} and Exception Message: {1}", e.GetType().Name,e.Message);
}
}
}

private static bool IsDivisibleBy2(int num)
{
if (num % 3 == 0) throw new ArgumentException(string.Format("The number {0} is divisible by 3", num));
return num % 2 == 0;
}

The preceding code block is writing all the details from an exception that was thrown in a PLINQ. Here, we are traversing and showcasing all six exceptions:

You can loop through the InnerExceptions property and take necessary actions. We will look at inner exceptions in more detail in Chapter 7, Implementing Exception Handling. However, in this case, when a PLINQ is executed, instead of terminating the execution on an exception, it will run through all the iterations and provide the final results.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.137.37