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.
- public static class Parallel
- {
- public static void Invoke(params Action[] actions);
- public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);
- }
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:
-
-
-
-
-
-
-
- [Fact]
- public void Test_Parallel_Invoke_Passing_An_Action_Delegate_And_CancellationToken()
- {
-
- int totalSpan = 0;
- int totalParagraph = 0;
- int totalLink = 0;
-
-
- Uri url = new Uri("https://www.microsoft.com/en-ph/");
-
-
- CancellationTokenSource tokenSource = new CancellationTokenSource();
- CancellationToken token = tokenSource.Token;
-
-
- ParallelOptions options = new ParallelOptions { CancellationToken = token };
-
- Func<CancellationToken, string, string, Uri, int> getTotalElement = delegate (CancellationToken token, string message, string element, Uri url)
- {
- this._output.WriteLine(message);
- token.ThrowIfCancellationRequested();
- var client = new WebClient();
- var text = client.DownloadString(url.AbsoluteUri);
- var doc = new HtmlDocument();
- doc.LoadHtml(text);
- return doc.DocumentNode.SelectNodes(element).Count();
- };
-
-
- tokenSource.Cancel();
-
- try
- {
- Parallel.Invoke(options,
- () => totalSpan = getTotalElement(token, "started to get the span element", "//span", url),
- () => totalParagraph = getTotalElement(token, "started to get the paragraph element", "//p", url),
- () => totalLink = getTotalElement(token, "started to get and count total link element", "//link", url));
- }
- catch (OperationCanceledException operationCanceled)
- {
- this._output.WriteLine(operationCanceled.Message);
- }
- Assert.True(totalLink == 0);
- Assert.True(totalParagraph == 0);
- Assert.True(totalSpan == 0);
- }
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:
-
-
-
- [Fact]
- public void Test_Parallel_Invoke_Passing_An_Action_And_HandleException()
- {
-
-
- int totalSpan = 0;
- int totalParagraph = 0;
- int totalLink = 0;
-
-
- Uri url = new Uri("https://www.microsoft.com/en-ph/");
-
-
-
-
- Func<string, string, Uri, int> getTotalElement = delegate (string message, string element, Uri url)
- {
- throw new Exception($"Random exception from {element}");
- };
-
- #region prove-that-parallel-invoke-throws-an-aggregate-exception
- var exception = Assert.Throws<AggregateException>(() => {
-
- Parallel.Invoke(
- () => totalSpan = getTotalElement("started to get the span element", "//span", url),
- () => totalParagraph = getTotalElement("started to get the paragraph element", "//p", url),
- () => totalLink = getTotalElement("started to get and count total link element", "//link", url));
-
- });
-
- Assert.True(((AggregateException)exception).Flatten().InnerExceptions.All(x => x.Message.Contains("Random exception")));
-
- #endregion
-
-
- try
- {
- Parallel.Invoke(
- () => totalSpan = getTotalElement("started to get the span element", "//span", url),
- () => totalParagraph = getTotalElement("started to get the paragraph element", "//p", url),
- () => totalLink = getTotalElement("started to get and count total link element", "//link", url));
- }
- catch (AggregateException aggregateException)
- {
- foreach (Exception error in aggregateException.Flatten().InnerExceptions)
- {
- this._output.WriteLine(error.Message);
- }
- }
-
- }
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:
-
-
-
-
-
-
-
- [Fact]
- public void Test_Parallel_Invoke_Passing_An_Action_Delegate_And_MaxDegreeOfParallelism()
- {
-
- int totalSpan = 0;
- int totalParagraph = 0;
- int totalLink = 0;
- int totalDiv = 0;
-
-
- Uri url = new Uri("https://www.microsoft.com/en-ph/");
-
-
-
- ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 2 };
-
- Func<string, string, Uri, int> getTotalElement = delegate (string message, string element, Uri url)
- {
- this._output.WriteLine($"{message} and Task Id: {Task.CurrentId}");
- var client = new WebClient();
- var text = client.DownloadString(url.AbsoluteUri);
- var doc = new HtmlDocument();
- doc.LoadHtml(text);
- return doc.DocumentNode.SelectNodes(element).Count();
- };
-
- try
- {
- Parallel.Invoke(options,
- () => totalSpan = getTotalElement("started to get the span element", "//span", url),
- () => totalParagraph = getTotalElement("started to get the paragraph element", "//p", url),
- () => totalLink = getTotalElement("started to get and count total link element", "//link", url),
- () => totalDiv = getTotalElement("started to get and count div element", "//div", url));
- }
- catch (OperationCanceledException operationCanceled)
- {
- this._output.WriteLine(operationCanceled.Message);
- }
- catch(AggregateException aggregateException)
- {
- foreach (Exception error in aggregateException.Flatten().InnerExceptions)
- {
- this._output.WriteLine(error.Message);
- }
- }
-
- Assert.True(totalLink > 0);
- Assert.True(totalParagraph > 0);
- Assert.True(totalSpan > 0);
- Assert.True(totalDiv > 0);
- }
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:
-
-
-
- [Fact]
- public void Test_Parallel_For()
- {
- this._output.WriteLine("--------Start--------");
- this._output.WriteLine("Warning: Not in order");
-
- Parallel.For(0, 30, counter => { this._output.WriteLine($"Counter = {counter}, Task Id: {Task.CurrentId}"); });
-
- this._output.WriteLine("--------End--------");
- }
-
-
-
- [Fact]
- public void Test_Parallel_Nested_For()
- {
- this._output.WriteLine("--------Start--------");
- this._output.WriteLine("Warning: Not in order");
-
- Parallel.For(0, 10, counter =>
- {
- this._output.WriteLine($"Counter = {counter}, Task Id: {Task.CurrentId}");
-
- Parallel.For(0, counter, innerCounter => {
- this._output.WriteLine($"Counter = {counter} at inner-counter-loop = {innerCounter}, Task Id: {Task.CurrentId}");
- });
- });
-
- this._output.WriteLine("--------End--------");
- }
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:
-
-
-
- [Fact]
- public void Test_Parallel_ForEach()
- {
- IEnumerable<int> range = Enumerable.Range(0, 100);
- Parallel.ForEach(range, counter => {
-
- this._output.WriteLine($"{counter}");
- });
- }
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.
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()
-
-
-
- [Fact]
- public void Test_Parallel_ForEach_Parallel_LoopState_Break()
- {
- var customers = new List<dynamic>
- {
- new { FirstName="Mark", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },
- new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },
- new { FirstName="Jin", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) },
- new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },
- };
-
- ParallelLoopResult result = Parallel.ForEach(customers, (customer, loopState) => {
-
- this._output.WriteLine($"{customer.LastName}, {customer.FirstName} is below 18. Current age {customer.Age}");
-
- if (loopState.IsStopped) return;
-
- if (customer.Age < 18)
- {
- this._output.WriteLine($"Breaking at the loop");
-
- loopState.Break();
- }
- });
-
- Assert.True(!result.IsCompleted);
- Assert.True(result.LowestBreakIteration == 1);
- Assert.True(result.LowestBreakIteration != 0);
- }
-
-
-
- [Fact]
- public void Test_Parallel_For_Parallel_LoopState_Break()
- {
- var customers = new List<dynamic>
- {
- new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },
- new { FirstName="Mark", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },
- new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },
- new { FirstName="Jin", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) }
-
- };
-
- var customerRange = Enumerable.Range(0, customers.Count);
-
- ParallelLoopResult result = Parallel.For(0, customers.Count, (customerIndex, loopState) => {
-
- this._output.WriteLine($"{customers[customerIndex].LastName}, {customers[customerIndex].FirstName} is below 18. Current age {customers[customerIndex].Age}");
-
- if (loopState.IsStopped) return;
-
- if (customers[customerIndex].Age < 18)
- {
- this._output.WriteLine($"Breaking at the loop");
-
- loopState.Break();
- }
- });
-
- Assert.True(!result.IsCompleted);
- Assert.True(result.LowestBreakIteration == 2);
- Assert.True(result.LowestBreakIteration != 0);
- }
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()
-
-
-
- [Fact]
- public void Test_Parallel_ForEach_Parallel_LoopState_Stop()
- {
- var customers = new List<dynamic>
- {
- new { FirstName="Mark", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },
- new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },
- new { FirstName="Jin", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) },
- new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },
- };
-
- ParallelLoopResult result = Parallel.ForEach(customers, (customer, loopState) => {
-
- this._output.WriteLine($"{customer.LastName}, {customer.FirstName} is below 18. Current age {customer.Age}");
-
- if (loopState.IsStopped) return;
-
- if (customer.Age < 18)
- {
- this._output.WriteLine($"Breaking at the loop");
- loopState.Stop();
- }
- });
- Assert.True(!result.IsCompleted);
- Assert.True(!result.LowestBreakIteration.HasValue);
- }
-
-
-
- [Fact]
- public void Test_Parallel_For_Parallel_LoopState_Stop()
- {
- var customers = new List<dynamic>
- {
- new { FirstName="Vincent", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-20).Year) },
- new { FirstName="Mark", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-18).Year) },
- new { FirstName="Anthony", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-14).Year) },
- new { FirstName="Jin", LastName ="Necesario", Birthdate = DateTime.Now, Age = (DateTime.Now.Year - DateTime.Now.AddYears(-13).Year) }
-
- };
-
- var customerRange = Enumerable.Range(0, customers.Count);
-
- ParallelLoopResult result = Parallel.For(0, customers.Count, customerIndex, loopState) => {
-
- this._output.WriteLine($"{customers[customerIndex].LastName}, {customers[customerIndex].FirstName} is below 18. Current age {customers[customerIndex].Age}");
-
- if (loopState.IsStopped) return;
-
- if (customers[customerIndex].Age < 18)
- {
- this._output.WriteLine($"Breaking at the loop");
-
- loopState.Stop();
- }
- });
-
- Assert.True(!result.IsCompleted);
- Assert.True(!result.LowestBreakIteration.HasValue);
-
- }
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!