Simple PLINQ example

I love LINQ. From the onset, I liked the idea of having a simple and similar way of accessing any kind of data, be it objects, be it SQL data or xml or any other structured form. MSDN says that PLINQ is a parallel implementation of LINQ to objects. In this blog, I am providing a simple sample about one of the operators for PLINQ.
Advantage of using PLINQ, as with any other multi-threading scenario, is performance. First up, lets see the code to achieve that performance gain - 
private static void SimpleExample()
        {
            var people = new List<string>();
            Load(people);
            var namesStartingWithN = new List<string>();

            List<long> times = new List<long>();

            for (int i = 0; i < 10; i++)
            {
                System.Diagnostics.Stopwatch sp = new Stopwatch();
                sp.Start();
                namesStartingWithN = people
                    // run in parallel
                    .AsParallel()
                    .Where(p => p.StartsWith("N"))
                    .ToList();

                sp.Stop();
                long et = sp.ElapsedMilliseconds;
                times.Add(et);
                Console.WriteLine("Total Time: " + et);
            }

            Console.WriteLine();
            Console.WriteLine("Average: " + times.Average());
        }

        static void Load(List<string> people)
        {
            Random rand = new Random();
            for (int i = 0; i < 1000000; i++)
            {
                int r = rand.Next(1, 5);
                string name = string.Empty;
                switch (r)
                {
                    case 1:
                        name = "Michael" + i;
                        break;
                    case 2:
                        name = "Sachin" + i;
                        break;
                    case 3:
                        name = "Roger" + i;
                        break;
                    case 4:
                        name = "Nadal" + i;
                        break;
                    case 5:
                    default:
                        name = "Pete" + i;
                        break;
                }

                people.Add(name);
            }
        }
In the Load method, I just create a large number of string objects which represent different names. Then, in the SimpleExample method, my goal is to find all the names that start with "N". This can be done by a simple LINQ query. But if you observe the LINQ query, I have also added "AsParallel()" operator. This instructs the compiler to run it in parallel. The outer for loop (which runs 10 times) is just for calculating the average time it takes with and without "AsParallel()".
Without "AsParallel()"


With "AsParallel()"


The results might differ from one machine configuration to other, but, its pretty evident that, there is a good performance gain.
Sometimes, we need to get results in a particular order. In this example, it was not necessary however if you need to return results in order, you can call the "AsOrdered()" operator after the "AsParallel()" operator.
The other important aspect about "AsParallel()" is that we can not change all our existing/new queries to have "AsParallel()" to gain performance. The reason for this is, if used wrongly, there is a good chance of race condition. We should analyze the usage of "AsParallel()" accordingly. Code below is a good example of race condition - 
private static void RaceCondition()
        {
            for (int i = 0; i < 10; i++)
            {
                int sum = 0;
                try
                {
                    Enumerable.Range(1, 100000)
                        // This statement causes race condition
                        .AsParallel()               
                        .Select(n =>
                        {
                            sum += n;
                            return sum;
                        })
                        .ToList();
                }
                catch (AggregateException ex)
                {
                    foreach (Exception e in ex.InnerExceptions)
                    {
                        Console.WriteLine(e.Message);
                    }
                }
                Console.WriteLine(sum);
            }
        }
Without "AsParallel()"

With "AsParallel()"
As you can see the sum which was correct when we didn't use "AsParallel()" is now incorrect and inconsistent.

No comments: