Today’s computers are likely to have multicore processors with four or eight cores. One vendor already offers a 61-core processor3 and future processors are likely to have many more cores. Your computer’s operating system shares the cores among operating system tasks and the apps running at a given time.
A concept called threads enables the operating system to run parts of an app concurrently—for example, while the user-interface thread (commonly called the GUI thread) waits for the user to interact with a GUI control, separate threads in the same app could be performing other tasks like complex calculations, downloading a video, playing music, sending an e-mail, etc. Though all of these tasks can make progress concurrently, they may do so by sharing one processor core.
With multicore processors, apps can operate truly in parallel (that is, simultaneously) on separate cores. In addition, the operating system can allow one app’s threads to operate truly in parallel on separate cores, possibly increasing the app’s performance substantially. Parallelizing apps and algorithms to take advantage of multiple cores is difficult and highly error prone, especially if those tasks share data that can be modified by one or more of the tasks.
In Section 21.10, we mentioned that a benefit of functional programming and internal iteration is that the library code (behind the scenes) iterates through all the elements of a collection to perform a task. Another benefit is that you can easily ask the library to perform a task with parallel processing to take advantage of a processor’s multiple cores. This is the purpose of PLINQ (Parallel LINQ)—an implementation of the LINQ to Objects extension methods that parallelizes the operations for increased performance. PLINQ handles for you the error-prone aspects of breaking tasks into smaller pieces that can execute in parallel and coordinating the results of those pieces, making it easier for you to write high-performance apps that take advantage of multicore processors.
Figure 21.9 demonstrates the LINQ to Objects and PLINQ extension methods Min
, Max
and Average
operating on a 10,000,000-element array of random int
values (created in lines 13–15). As with LINQ to Objects, the PLINQ versions of these operations do not modify the original collection. We time the operations to show the substantial performance improvements of PLINQ (using multiple cores) over LINQ to Objects (using a single core). For the remainder of this discussion we refer to LINQ to Objects simply as LINQ.
int
s with Enumerable Method Range
Using functional techniques, lines 13–15 create an array of 10,000,000 random int
s. Class Enumerable
provides static
method Range
to produce an IEnumerable<int>
containing integer values. The expression
Enumerable.Range(1, 10000000)
produces an IEnumerable<int>
containing the values 1 through 10,000,000—the first argument specifies the starting value in the range and the second specifies the number of values to produce. Next, line 14 uses LINQ extension method Select
to map every element to a random integer in the range 1–999. The lambda
x => random.Next(1, 1000)
ignores its parameter (x
) and simply returns a random value that becomes part of the IEnumerable<int>
returned by Select
. Finally, line 15 calls extension method ToArray
, which returns an array of int
s containing the elements produced by the Select
operation. Note that class Enumerable
also provides extension method ToList
to obtain a List<T>
rather than an array.
Min
, Max
and Average
with LINQTo calculate the total time required for the LINQ Min
, Max
and Average
extension-method calls, we use type DateTime
’s Now
property to get the current time before (line 20) and after (line 24) the LINQ operations. Lines 21–23 perform Min
, Max
and Average
on the array values
. Line 27 uses DateTime
method Subtract
to compute the difference between the end and start times, which is returned as a TimeSpan
. We then store the TimeSpan
’s Total-Milliseconds
value for use in a later calculation showing PLINQ’s percentage improvement over LINQ.
Min
, Max
and Average
with PLINQLines 33–41 perform the same tasks as lines 20–28, but use PLINQ Min
, Max
and Average
extension-method calls to demonstrate the performance improvement over LINQ. To initiate parallel processing, lines 34–36 invoke IEnumerable<T>
extension method AsParallel
(from class ParallelEnumerable
), which returns a ParallelQuery<T>
(in this example, T
is int
). An object that implements ParallelQuery<T>
can be used with PLINQ’s parallelized versions of the LINQ extension methods. Class ParallelEnumerable
defines the ParallelQuery<T>
(PLINQ) parallelized versions as well as several PLINQ-specific extension methods. For more information on the extension methods defined in class ParallelEnumerable
, visit
https://msdn.microsoft.com/library/system.linq.parallelenumerable
Lines 44–46 calculate and display the percentage improvement in processing time of PLINQ over LINQ. As you can see from the sample outputs, the simple choice of PLINQ (via AsParallel
) decreased the total processing time substantially—55% and 41% less time in the two sample outputs. We ran the app many more times (using up to three cores) and processing-time savings were generally within the 41–55% range—though we did have a 61% savings on one sample execution. The overall savings will be affected by your processor’s number of cores and by what else is running on your computer.