Pulled Apart - Part VII: PLINQ, not as easy as first assumed

[Pull Icon]

Note: This is part of a series, you can find the rest of the parts in the series index.

PLINQ, which is Parallel LINQ—or the ability to run LINQ queries with parallel extensions in .NET 4—is supposed to magically parallelize queries by appending .AsParallel(). Here’s my infamous "insane solution" to Fizz Buzz:

var result = from i in Enumerable.Range(0, 1000).AsParallel()
             where (i % 3 == 0 || i % 5 == 0)
             select new { value = i, answer = i % 3 == 0 ? i % 5 == 0 ? "Fizz Buzz" : "Buzz" : "Fizz" };

foreach (var item in result)
{
    Console.WriteLine("{0} gets a {1}", item.value, item.answer);
}

If you’ve attended one of my What’s New in .NET 4 talks, you might have seen me demo this—and for that, I am VERY VERY SORRY, because I was wrong. 🙁

In Pull, I used this exact approach to parallelize podcast updates. It wasn’t until I implemented a status view that I realized—two weeks and 46 check-ins later—it wasn’t actually parallel.

The issue? .AsParallel() alone does little more than setup. For true parallelism, you need .ForAll(), as shown below (note the processing change on line 5):

var result = from i in Enumerable.Range(0, 30).AsParallel()
             where (i % 3 == 0 || i % 5 == 0)
             select new { value = i, answer = i % 3 == 0 ? i % 5 == 0 ? "Fizz Buzz" : "Buzz" : "Fizz" };

result.ForAll(item =>
{
    Console.WriteLine("{0} gets a {1}", item.value, item.answer);
});

Now Pull truly runs in parallel. Problem solved? Wrong again. Further research led me to a white paper by Pamela Vagata (Microsoft’s Parallel Computing Platform Group). It clearly explains when to use PLINQ vs. Parallel.ForEach:

ActionPLINQParallel.ForEach
Simple Data-Parallel Operation with Independent Actions
Ordered Data-Parallel Operation
Streaming Data-Parallel Operation
Operating over Two Collections
Thread-Local State
Exiting from Operations

My mistake? I used PLINQ for a Simple Data-Parallel Operation with Independent Actions—a perfect case for Parallel.ForEach. Why?

While PLINQ’s ForAll exists, parallel loops are often more intuitive for independent actions. PLINQ’s overhead can be excessive for simple tasks. With Parallel.ForEach, you control thread count via MaxDegreeOfParallelism, adapting to system resources. PLINQ, however, requires exact thread counts via WithDegreeOfParallelism(), locking in resource usage.

Final Thoughts

.NET 4 made parallelism too easy—so easy that wrong choices still appear to work. Research matters. Don’t assume.