Using a parallel foreach loop to run multiple threads

A while ago, during a work retreat (yes, the company I work for is really that cool), Graham Rook, who is one of my colleagues, showed me a parallel foreach loop. It certainly speeds up processing a great deal. But here's the rub. It makes no sense using a parallel foreach loop if you're dealing with small amounts of data or little tasks. The parallel foreach loop excels when there is bulk processing to do or huge amounts of data to process.

Getting ready

We will start off by looking at where the parallel foreach loop does not perform better than the standard foreach loop. For this, we will create a small list of 500 items and just iterate over the list, writing the items to the console window.

For the second example that illustrates the power of the parallel foreach loop, we will use the same list and create a file for each item in the list. The power and benefit of the parallel foreach loop will be evident in the second example.

How to do it…

  1. Start off by creating two methods in the Recipes class. Call one method ReadCollectionForEach() and pass it a parameter of List<string>. Create a second method called ReadCollectionParallelForEach() that also accepts a parameter of List<string>:
    public class Recipes
    {
        public double ReadCollectionForEach(List<string> intCollection)
        {        
    
        }
    
        private double ReadCollectionParallelForEach(List<string> intCollection)
        {        
    
        }
    }
  2. In the ReadCollectionForEach() method, add a standard foreach loop that will iterate over the collection of strings passed to it and write the value it finds to the console window. Then, clear the console window. Use a timer to keep track of the total seconds elapsed during the foreach loop:
    var timer = Stopwatch.StartNew();
    foreach (string integer in intCollection)
    {
        WriteLine(integer);
        Clear();
    }
    return timer.Elapsed.TotalSeconds;
  3. In the second method, called ReadCollectionParallelForEach(), do the same. However, instead of using a standard foreach loop, add a Parallel.ForEach loop. You will notice that the Parallel.ForEach loop looks slightly different. The signature of Parallel.ForEach requires that you pass it an enumerable data source (List<string> intCollection) and define an action, which is the delegate that is invoked for every iteration (integer):
    var timer = Stopwatch.StartNew();
    Parallel.ForEach(intCollection, integer =>
    {
        WriteLine(integer);
        Clear();
    });
    return timer.Elapsed.TotalSeconds;
  4. When you have added all the required code, your Recipes class should look like this:
    public class Recipes
    {
        public double ReadCollectionForEach(List<string> intCollection)
        {        
            var timer = Stopwatch.StartNew();
            foreach (string integer in intCollection)
            {
                WriteLine(integer);
                Clear();
            }
            return timer.Elapsed.TotalSeconds;
        }
    
        public double ReadCollectionParallelForEach(List<string> intCollection)
        {        
            var timer = Stopwatch.StartNew();
            Parallel.ForEach(intCollection, integer =>
            {
                WriteLine(integer);
                Clear();
            });
            return timer.Elapsed.TotalSeconds;
        }
    }
  5. In the console application, create the List<string> collection and pass it to the two methods created in the Recipes class. You will notice that we are only creating a collection of 500 items. After the code is completed, return the time elapsed in seconds and output it to the console window:
    List<string> integerList = new List<string>();
    for (int i = 0; i <= 500; i++)
    {
        integerList.Add(i.ToString());
    }
    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    double timeElapsed1 = oRecipe.ReadCollectionForEach(integerList);
    double timeElapsed2 = oRecipe.ReadCollectionParallelForEach(integerList);
    WriteLine($"foreach executed in {timeElapsed1}");
    WriteLine($"Parallel.ForEach executed in {timeElapsed2}");
  6. Run your application. From the output displayed, you will see that the performance gain using the Parallel.ForEach loop is negligible. In fact, in this case, the Parallel.ForEach loop only improved performance by 0.4516 percent:
    How to do it…
  7. Let's use a different example now. We will create a process-intensive task and measure the performance gain that the Parallel.ForEach loop will give us. Create two methods called CreateWriteFilesForEach() and CreateWriteFilesParallelForEach(), which both take the List<string> collection as the parameter:
    public class Recipes
    {
        public void CreateWriteFilesForEach(List<string> intCollection)
        {        
    
        }
    
        private void CreateWriteFilesParallelForEach(List<string> intCollection)
        {        
    
        }
    }
  8. Add the following code to the CreateWriteFilesForEach() method. This code starts the timer and executes the standard foreach loop on the List<string> object. It then writes the elapsed time out to the console window:
    WriteLine($"Start foreach File method");
    var timer = Stopwatch.StartNew();
    foreach (string integer in intCollection)
    {    
    
    }
    WriteLine($"foreach File method executed in {timer.Elapsed.TotalSeconds} seconds");
  9. Inside the foreach loop, add the code to check whether a file exists with the specific name created by appending the integer value to the filename portion of the filePath variable. Create the file (ensuring that you use Dispose method in order not to lock the file when trying to write to it) and write some text to the newly created file:
    string filePath = $"C:\temp\output\ForEach_Log{integer}.txt";
    if (!File.Exists(filePath))
    {
        File.Create(filePath).Dispose();
        using (StreamWriter sw = new StreamWriter(filePath, false))
        {
            sw.WriteLine($"{integer}. Log file start: {DateTime.Now.ToUniversalTime().ToString()}");
        }
    }
  10. Next, add this code to the CreateWriteFilesParallelForEach() method, which basically performs the same function as the CreateWriteFilesForEach() method, but uses a Parallel.ForEach loop to create and write files:
    WriteLine($"Start Parallel.ForEach File method");
    var timer = Stopwatch.StartNew();
    Parallel.ForEach(intCollection, integer =>
    {
                        
    });
    WriteLine($"Parallel.ForEach File method executed in {timer.Elapsed.TotalSeconds} seconds");
  11. Add the slightly modified file-creation code inside the Parallel.ForEach loop:
    string filePath = $"C:\temp\output\ParallelForEach_Log{integer}.txt";
    if (!File.Exists(filePath))
    {
        File.Create(filePath).Dispose();
        using (StreamWriter sw = new StreamWriter(filePath, false))
        {
            sw.WriteLine($"{integer}. Log file start: {DateTime.Now.ToUniversalTime().ToString()}");
        }
    }
  12. When you are done, your code needs to look like this:
    public class Recipes
    {
        public void CreateWriteFilesForEach(List<string> intCollection)
        {        
            WriteLine($"Start foreach File method");
            var timer = Stopwatch.StartNew();
            foreach (string integer in intCollection)
            {
                string filePath = $"C:\temp\output\ForEach_Log{integer}.txt";
                if (!File.Exists(filePath))
                {
                    File.Create(filePath).Dispose();
                    using (StreamWriter sw = new StreamWriter(filePath, false))
                    {
                        sw.WriteLine($"{integer}. Log file start: {DateTime.Now.ToUniversalTime() .ToString()}");
                    }
                }
            }
            WriteLine($"foreach File method executed in {timer.Elapsed.TotalSeconds} seconds");
        }
    
        public void CreateWriteFilesParallelForEach(List<string> intCollection)
        {        
            WriteLine($"Start Parallel.ForEach File method");
            var timer = Stopwatch.StartNew();
            Parallel.ForEach(intCollection, integer =>
            {
                string filePath = $"C:\temp\output\ParallelForEach_Log {integer}.txt";
                if (!File.Exists(filePath))
                {
                    File.Create(filePath).Dispose();
                    using (StreamWriter sw = new StreamWriter(filePath, false))
                    {
                        sw.WriteLine($"{integer}. Log file start: {DateTime.Now.ToUniversalTime()
                        .ToString()}");
                    }
                }                
            });
            WriteLine($"Parallel.ForEach File method executed in {timer.Elapsed.TotalSeconds} seconds");
        }
    }
  13. Heading over to the console application, modify the List<string> object slightly and increase the count from 500 to 1000. Then, call the file methods created in the Recipes class:
    List<string> integerList = new List<string>();
    for (int i = 0; i <= 1000; i++)
    {
        integerList.Add(i.ToString());
    }
    
    Chapter7.Recipes oRecipe = new Chapter7.Recipes();
    oRecipe.CreateWriteFilesForEach(integerList);
    oRecipe.CreateWriteFilesParallelForEach(integerList);
    ReadLine();
  14. Finally, when you are ready, make sure that you have the C: empoutput directory and that there aren't any other files in that directory. Run your application and review the output to the console window. This time round, we can see that the Parallel.ForEach loop has made a huge difference. The performance gain is massive and heralds a 60.7074 percent performance increase over the standard foreach loop:
    How to do it…

How it works…

From the examples used in this recipe, it is clear that the use of the parallel foreach loop should be considered carefully. If you are dealing with relatively low volumes of data or non-process intensive transactions, the parallel foreach loop will not benefit your application's performance much. In some instances, the standard foreach loop could be much faster than the parallel foreach loop. If, however, you find your application running into performance issues when processing large amounts of data or running processor-intensive tasks, give the parallel foreach loop a try. It just might surprise you.

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

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