Understanding Parallel Class (Parallel Loops) using C#

Introduction & Background

 
In this article, we are going to focus on the Parallel Class. However; before everything else, I assume that you are familiar with the basic looping constructs of the C# language. As we all know, these looping constructs such as while, do-while, for, for-each are one of the foundations of the language. Furthermore; these looping constructs are sequential loops by nature because an iteration isn’t started until the previous iteration has completed. I’m also guessing that you have at least abused these looping constructs sometime in your programming career because it shows your curiosity in learning the language.
 
Now, going back to Parallel loops. The Task Parallel Library (TPL) supports it. Even I was fascinated by it at first. Moreover; this library provides a common replacement to the sequential loops that we are familiar with and this article will focus on that.
 
Here is the list of topics of this article
  • What is the classic fork/join model?
  • What is a Parallel class?
  • How to use Parallel. Invoke?
  • How to use Parallel.For?
  • What's ParallelLoopState and ParallelLoopResult?

What is Classic Fork/Join Model?

 
We can define the classic fork/join model by saying: “It is a way of configuring and executing your parallel programs, in such a way the executed one departs off in parallel and then merge at a certain point and gets back to sequential execution of your program.”. Wow! Jargon. For me just imagine that your program is running 10 methods in parallel (in any order) and at some point, these running methods need to join at a certain point so that your program will resume its default execution mode which is sequential. If ever I didn’t explain it well, you can go directly to Wikipedia using this link.
 
To see these in action, you can immediately go straight ahead to the code samples below.
 

What is a Parallel Class?

 
The parallel class resides within the namespace of System.Threading.Tasks and it provides support for parallel loops. Furthermore; it mimics the sequential loops that most developers are used to. And these are the Parallel. Invoke, Parallel. For and Parallel.ForEach.
 

How to use Parallel.Invoke?

 
Before jumping into the code sample, let’s begin to see what parameters are available for us to use.
  1. public static class Parallel      
  2.     {      
  3.         public static void Invoke(params Action[] actions);      
  4.         public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);      
  5.     }     
    As we can see the first parameter is an array of delegate-Action. Once you have passed the proper argument to the first given parameter the Parallel.Invoke will basically launch these Actions and run in parallel and once all are done the continue the classic fork/join model.
     
    See the example below:
      What is the ParallelOptions class?
       
      Basically, this class helps developers to set the configuration which will influence the behavior of the methods inside the Parallel class. Furthermore; this class has three (3) properties which are CancellationToken, MaxDegreeOfParallelism, and TaskScheduler. However; we are just going to discuss the usage of CancellationToken and MaxDegreeOfParallelism only.
       

      CancellationToken 

       
      It helps you as a developer to control the termination of the parallel invocation/execution. See the example below:
      1. /// <summary>    
      2.     /// In this example we are going to download the an html page from Microsoft page having this url 'https://www.microsoft.com/en-ph/'.    
      3.     /// We are going to count the number of span, paragraph and links in the page in parallel.    
      4.     /// When you debug this sample unit test you'll see that it behaves differently.     
      5.     /// Meaning it can run in order based on the delegate actions that we passed. In our case we only passed 3 Action delegates.    
      6.     /// However; will be cancelling the functionality.     
      7.     /// </summary>    
      8.     [Fact]    
      9.     public void Test_Parallel_Invoke_Passing_An_Action_Delegate_And_CancellationToken()    
      10.     {    
      11.         //Define the values needed when the parallel.invoke ended and joined with the main thread.    
      12.         int totalSpan = 0;    
      13.         int totalParagraph = 0;    
      14.         int totalLink = 0;    
      15.         
      16.         //define the URL needed to count the following elements: span, paragraph, and link.    
      17.         Uri url = new Uri("https://www.microsoft.com/en-ph/");    
      18.         
      19.         //Define the cancellation token source we need to cancel even before the parallel.invoke is executed    
      20.         CancellationTokenSource tokenSource = new CancellationTokenSource();    
      21.         CancellationToken token = tokenSource.Token;    
      22.         
      23.         //Define the ParallelOptions class to be passed as an argument to the first parameter of Parallel.Invoke    
      24.         ParallelOptions options = new ParallelOptions { CancellationToken = token };    
      25.         
      26.         Func<CancellationToken, stringstring, Uri, int> getTotalElement = delegate (CancellationToken token, string message, string element, Uri url)    
      27.         {    
      28.             this._output.WriteLine(message);    
      29.             token.ThrowIfCancellationRequested();          
      30.             var client = new WebClient();         
      31.             var text = client.DownloadString(url.AbsoluteUri);          
      32.             var doc = new HtmlDocument();          
      33.             doc.LoadHtml(text);          
      34.             return doc.DocumentNode.SelectNodes(element).Count();    
      35.         };    
      36.         
      37.         //you can uncomment this to see the full action    
      38.         tokenSource.Cancel(); //lets cancel    
      39.         
      40.         try    
      41.         {    
      42.             Parallel.Invoke(options,    
      43.             () => totalSpan = getTotalElement(token, "started to get the span element""//span", url),    
      44.             () => totalParagraph = getTotalElement(token, "started to get the paragraph element""//p", url),    
      45.             () => totalLink = getTotalElement(token, "started to get and count total link element""//link", url));    
      46.         }    
      47.         catch (OperationCanceledException operationCanceled)    
      48.         {    
      49.             this._output.WriteLine(operationCanceled.Message);    
      50.         }    
      51.         Assert.True(totalLink == 0);    
      52.         Assert.True(totalParagraph == 0);    
      53.         Assert.True(totalSpan == 0);      
      54. }   
        Now, that we have seen how to basically use the Parallel. Invoke just by passing an array of Action-delegates and passing a token for the termination, I would like to point out that we also need to learn how to handle the exceptions. See the example below: 
        1. /// <summary>    
        2. /// In this example we are going to throw and handle an exception from the Action delegates    
        3.     /// </summary>    
        4.     [Fact]    
        5.     public void Test_Parallel_Invoke_Passing_An_Action_And_HandleException()    
        6.     {    
        7.         //Define the values needed when the Parallel.Invoke ended and joined with the main thread.    
        8.         
        9.         int totalSpan = 0;    
        10.         int totalParagraph = 0;    
        11.         int totalLink = 0;    
        12.         
        13.         //define the URL needed to count the following elements: span, paragraph, and link.    
        14.         Uri url = new Uri("https://www.microsoft.com/en-ph/");    
        15.         
        16.         /*We no longer finish the method to its proper functionality.  
        17.         * We just need to throw an exception and see how it can be handled.  
        18.         */    
        19.         Func<stringstring, Uri, int> getTotalElement = delegate (string message, string element, Uri url)    
        20.         {    
        21.             throw new Exception($"Random exception from {element}");    
        22.         };    
        23.       
        24.         #region prove-that-parallel-invoke-throws-an-aggregate-exception    
        25.         var exception = Assert.Throws<AggregateException>(() => {    
        26.         
        27.             Parallel.Invoke(    
        28.                 () => totalSpan = getTotalElement("started to get the span element""//span", url),    
        29.                 () => totalParagraph = getTotalElement("started to get the paragraph element""//p", url),    
        30.                 () => totalLink = getTotalElement("started to get and count total link element""//link", url));    
        31.         
        32.         });     
        33.         //let's assert if all messages are equal to 'Random exception'    
        34.         Assert.True(((AggregateException)exception).Flatten().InnerExceptions.All(x => x.Message.Contains("Random exception")));    
        35.       
        36.         #endregion    
        37.         
        38.         //in this section will try to handle the exception via the try-catch code block    
        39.         try    
        40.         {    
        41.             Parallel.Invoke(    
        42.             () => totalSpan = getTotalElement("started to get the span element""//span", url),    
        43.             () => totalParagraph = getTotalElement("started to get the paragraph element""//p", url),    
        44.             () => totalLink = getTotalElement("started to get and count total link element""//link", url));    
        45.         }    
        46.         catch (AggregateException aggregateException)    
        47.         {    
        48.             foreach (Exception error in aggregateException.Flatten().InnerExceptions)    
        49.             {    
        50.                 this._output.WriteLine(error.Message);    
        51.             }    
        52.         }    
        53.         //end of try-catch code block    
        54.     }   

          MaxDegreeOfParallelism

           
          If you wish to limit the number of tasks that can be used for a given parallel invocation, and thus most likely influence the number of cores that will be used, see the example below:
          1. /// <summary>    
          2. /// In this example we are going to download the HTML page from Microsoft page having this URL 'https://www.microsoft.com/en-ph/'.    
          3.     /// We are going to count the number of the span, paragraph, and links in the page in parallel.    
          4.     /// When you debug this sample unit test you'll see that it behaves differently.     
          5.     /// Meaning it can run in order based on the delegate actions that we passed. In our case, we only passed 4 Action delegates.    
          6.     /// However; we only need to invoke two methods at a time.     
          7.     /// </summary>    
          8.     [Fact]    
          9.     public void Test_Parallel_Invoke_Passing_An_Action_Delegate_And_MaxDegreeOfParallelism()    
          10.     {    
          11.         //Define the values needed when the parallel.invoke ended and joined with the main thread.    
          12.         int totalSpan = 0;    
          13.         int totalParagraph = 0;    
          14.         int totalLink = 0;    
          15.         int totalDiv = 0;    
          16.         
          17.         //define the URL needed to count the following elements: span, paragraph, and link.    
          18.         Uri url = new Uri("https://www.microsoft.com/en-ph/");    
          19.         
          20.         //Define the ParallelOptions class to be passed as an argument to the first parameter of Parallel.Invoke.     
          21.         //Then set the MaxDegreeOfParllelism to two (2)    
          22.         ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 2 };    
          23.         
          24.         Func<stringstring, Uri, int> getTotalElement = delegate (string message, string element, Uri url)    
          25.         {    
          26.             this._output.WriteLine($"{message} and Task Id: {Task.CurrentId}");    
          27.             var client = new WebClient();    
          28.             var text = client.DownloadString(url.AbsoluteUri);    
          29.             var doc = new HtmlDocument();    
          30.             doc.LoadHtml(text);    
          31.             return doc.DocumentNode.SelectNodes(element).Count();    
          32.         };    
          33.         
          34.         try    
          35.         {    
          36.             Parallel.Invoke(options,    
          37.             () => totalSpan = getTotalElement("started to get the span element""//span", url),    
          38.             () => totalParagraph = getTotalElement("started to get the paragraph element""//p", url),    
          39.             () => totalLink = getTotalElement("started to get and count total link element""//link", url),    
          40.             () => totalDiv = getTotalElement("started to get and count div element""//div", url));    
          41.         }    
          42.         catch (OperationCanceledException operationCanceled)    
          43.         {    
          44.             this._output.WriteLine(operationCanceled.Message);    
          45.         }    
          46.         catch(AggregateException aggregateException)    
          47.         {    
          48.             foreach (Exception error in aggregateException.Flatten().InnerExceptions)    
          49.             {    
          50.                 this._output.WriteLine(error.Message);    
          51.             }    
          52.         }    
          53.         
          54.         Assert.True(totalLink > 0);    
          55.         Assert.True(totalParagraph > 0);    
          56.         Assert.True(totalSpan > 0);    
          57.         Assert.True(totalDiv > 0);    
          58.     }   

            How to use Parallel.For?

             
            When you want to iterate through a sequence of numbers, for example, 1 to 30 in any order then we can use the Parallel.For. Just remember that the simple Parallel.For takes the start and end values of the loop, along with the Action<int> delegate which represents the body of the loop. See some examples below:
            1. /// <summary>    
            2. /// Let's just iterate 0 to 30 using the Parallel. For    
            3. /// </summary>    
            4. [Fact]    
            5. public void Test_Parallel_For()    
            6. {    
            7.     this._output.WriteLine("--------Start--------");    
            8.     this._output.WriteLine("Warning: Not in order");    
            9.       
            10.     Parallel.For(0, 30, counter => { this._output.WriteLine($"Counter = {counter}, Task Id: {Task.CurrentId}"); });    
            11.         
            12.     this._output.WriteLine("--------End--------");    
            13. }   
            1. /// <summary>    
            2. /// Let's just iterate 0 to 30 using the Parallel.For with nested Parallel.For loop    
            3. /// </summary>    
            4. [Fact]    
            5. public void Test_Parallel_Nested_For()    
            6. {    
            7.     this._output.WriteLine("--------Start--------");    
            8.     this._output.WriteLine("Warning: Not in order");    
            9.        
            10.     Parallel.For(0, 10, counter =>     
            11.                         {     
            12.                             this._output.WriteLine($"Counter = {counter}, Task Id: {Task.CurrentId}");    
            13.                                     
            14.                             Parallel.For(0, counter, innerCounter => {    
            15.                                 this._output.WriteLine($"Counter = {counter} at inner-counter-loop = {innerCounter}, Task Id: {Task.CurrentId}");    
            16.                             });    
            17.                         });    
            18.         
            19.     this._output.WriteLine("--------End--------");    
            20. }   

                How to use Parallel.ForEach?

                 
                Let me guess, you are probably thinking that Parallel.ForEach is the parallel equivalent of the for-each loop. If that’s your thinking you are exactly right. See example below:
                1. /// <summary>    
                2. /// Let's just iterate 0 to 100 by using Parallel.ForEach    
                3. /// </summary>    
                4. [Fact]    
                5. public void Test_Parallel_ForEach()    
                6. {    
                7.     IEnumerable<int> range = Enumerable.Range(0, 100);          
                8.    Parallel.ForEach(range, counter => {    
                9.         
                10.        this._output.WriteLine($"{counter}");    
                11.            });    
                12. }   

                  What's ParallelLoopState and ParallelLoopResult?

                   
                  The main usage of ParallelLoopState is to mimic the regular loops in the C# language, meaning you can leave the loop via the usage of break keyword. However; in the world of parallel loops, its body is represented by a delegate. Therefore; you can't use the break keyword. If you haven't read my blog about the jump statement, please see this link.
                  Now, to allow this behavior, the ParallelLoopState object is available as an overload in both Parallel. For and Parallel.ForEach. 
                   
                  Let us see the different properties and methods of the ParallelLoopState below.
                   
                  Name
                  Property
                  Method Descriptions 
                  IsExceptional
                   
                  Returns true if another transaction has thrown an exception which prevents the loop from fully complete.
                  IsStopped   Return true if another iteration has requested the loop to be stopped.
                  LowestBreakIteration   Returns a nullable long where the index iteration in which the break was called.
                  ShouldExitCurrentIteration   Returns true if the current iteration doesn't need to complete. Sets to true in case of stop/break have been issued.
                  Break   A method to terminate the loop and make sure that the lower iterations will be executed before the loop ends.
                  Stop   A method that attempts to end the loop as soon as possible. Once issued, no loop task will start a new iteration.
                   
                  The ParallelLoopResult is a struct that actually gives only two properties IsCompleted and LowestBreakIteration. The IsCompleted returns true when the loop ran into completion and LowestBreakIteration returns a nullable long where the index iteration in which the break was called.
                   

                  Difference between break keyword with the ParallelLoopState.Break()

                   
                  The main difference between the two is that, when using a regular loop, if you break on the iteration it will be immediately acted upon and no further iterations will occur. However; using the ParallelLoopState.Break() terminates the loop but makes sure that the lower iterations will be executed before the loop ends. 
                   
                  See more examples below about ParallelLoopState.Break()
                  1. /// <summary>    
                  2. /// Let's iterate through a list of customers and break    
                  3. /// </summary>    
                  4. [Fact]    
                  5. public void Test_Parallel_ForEach_Parallel_LoopState_Break()    
                  6. {    
                  7.     var customers = new List<dynamic>     
                  8.     {     
                  9.        new { FirstName="Mark",    LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },    
                  10.         new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },    
                  11.         new { FirstName="Jin",     LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) },    
                  12.         new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },    
                  13.     };    
                  14.         
                  15.     ParallelLoopResult result = Parallel.ForEach(customers, (customer, loopState) => {    
                  16.                 
                  17.         this._output.WriteLine($"{customer.LastName}, {customer.FirstName} is below 18. Current age {customer.Age}");    
                  18.         
                  19.        if (loopState.IsStopped) return;    
                  20.         
                  21.         if (customer.Age < 18)    
                  22.         {    
                  23.             this._output.WriteLine($"Breaking at the loop");    
                  24.         
                  25.             loopState.Break();    
                  26.         }    
                  27.    });    
                  28.         
                  29.     Assert.True(!result.IsCompleted);    
                  30.     Assert.True(result.LowestBreakIteration == 1);    
                  31.     Assert.True(result.LowestBreakIteration != 0);    
                  32. }   
                  1. /// <summary>    
                  2. /// Let's iterate through a list of customers and break    
                  3. /// </summary>    
                  4. [Fact]    
                  5. public void Test_Parallel_For_Parallel_LoopState_Break()    
                  6. {    
                  7.     var customers = new List<dynamic>    
                  8.     {    
                  9.         new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },    
                  10.         new { FirstName="Mark",    LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },    
                  11.         new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },    
                  12.         new { FirstName="Jin",     LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) }    
                  13.                 
                  14.     };    
                  15.         
                  16.     var customerRange = Enumerable.Range(0, customers.Count);    
                  17.         
                  18.     ParallelLoopResult result = Parallel.For(0, customers.Count,  (customerIndex, loopState) => {    
                  19.         
                  20.         this._output.WriteLine($"{customers[customerIndex].LastName}, {customers[customerIndex].FirstName} is below 18. Current age {customers[customerIndex].Age}");    
                  21.         
                  22.         if (loopState.IsStopped) return;    
                  23.         
                  24.         if (customers[customerIndex].Age < 18)    
                  25.         {    
                  26.             this._output.WriteLine($"Breaking at the loop");    
                  27.         
                  28.             loopState.Break();    
                  29.         }    
                  30.     });    
                  31.         
                  32.     Assert.True(!result.IsCompleted);    
                  33.     Assert.True(result.LowestBreakIteration == 2);    
                  34.     Assert.True(result.LowestBreakIteration != 0);    
                  35. }   

                  Difference between the ParallelLoopState.Break() with ParallelLoopState.Stop()

                   
                  Now, if you want to terminate the loop as soon as possible completely and don't care about the about guaranteeing the lower iterations have completed, then ParallelLoopState.Stop() comes to the rescue. Unlike, ParallelLoopState.Break() terminates the loop but makes sure that the lower iterations will be executed before the loop ends.
                   
                  See more examples below about ParallelLoopState.Stop() 
                  1. /// <summary>    
                  2. /// Let's iterate through a list of customers and stop    
                  3. /// </summary>    
                  4. [Fact]    
                  5. public void Test_Parallel_ForEach_Parallel_LoopState_Stop()    
                  6. {    
                  7.     var customers = new List<dynamic>    
                  8.     {    
                  9.         new { FirstName="Mark",    LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },    
                  10.         new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },    
                  11.         new { FirstName="Jin",     LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) },    
                  12.         new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },    
                  13.     };    
                  14.         
                  15.     ParallelLoopResult result = Parallel.ForEach(customers, (customer, loopState) => {    
                  16.         
                  17.         this._output.WriteLine($"{customer.LastName}, {customer.FirstName} is below 18. Current age {customer.Age}");    
                  18.         
                  19.         if (loopState.IsStopped) return;    
                  20.         
                  21.         if (customer.Age < 18)    
                  22.         {    
                  23.             this._output.WriteLine($"Breaking at the loop");          
                  24.             loopState.Stop();    
                  25.         }    
                  26. });    
                  27.        Assert.True(!result.IsCompleted);    
                  28.       Assert.True(!result.LowestBreakIteration.HasValue);    
                  29. }   
                  1. /// <summary>    
                  2. /// Let's iterate through a list of customers and stop    
                  3. /// </summary>    
                  4. [Fact]    
                  5. public void Test_Parallel_For_Parallel_LoopState_Stop()    
                  6. {    
                  7.     var customers = new List<dynamic>    
                  8.     {    
                  9.         new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },    
                  10.         new { FirstName="Mark",    LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },    
                  11.         new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },    
                  12.         new { FirstName="Jin",     LastName ="Necesario", Birthdate = DateTime.Now, Age =  (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) }    
                  13.         
                  14.     };    
                  15.         
                  16.     var customerRange = Enumerable.Range(0, customers.Count);    
                  17.         
                  18.     ParallelLoopResult result = Parallel.For(0, customers.Count, customerIndex, loopState) => {    
                  19.         
                  20.         this._output.WriteLine($"{customers[customerIndex].LastName}, {customers[customerIndex].FirstName} is below 18. Current age {customers[customerIndex].Age}");    
                  21.         
                  22.         if (loopState.IsStopped) return;    
                  23.         
                  24.         if (customers[customerIndex].Age < 18)    
                  25.         {    
                  26.             this._output.WriteLine($"Breaking at the loop");    
                  27.         
                  28.             loopState.Stop();    
                  29.         }    
                  30.     });    
                  31.         
                  32.     Assert.True(!result.IsCompleted);    
                  33.     Assert.True(!result.LowestBreakIteration.HasValue);    
                  34.         
                  35. }   

                  Summary and Remarks

                   
                  In this article, we have discussed the following,
                  • What is the classic fork/join model?
                  • What is a Parallel class?
                  • How to use Parallel. Invoke?
                  • How to use Parallel.For?
                  • What's ParallelLoopState and ParallelLoopResult?
                  I hope you have enjoyed this article, as I have enjoyed writing it. You can also find the sample code here at GitHub. Until next time, happy programming!