The Parallel class

The Parallel class is optimized for iterations and its behavior is even better—in loops—than PLINQ, although the difference is not meaningful. However, there are situations in which a fine-tuning of loops can noticeably increase the user experience.

The class has variants of the for and foreach methods (also invoke, but it is rare to see this in practice), which can be used in loops when we think the performance can be clearly slowed down using the nonparallel versions.

If we take a look at the definition of the Parallel.For version, we'll see that it receives a couple of numbers (int or long) to define the scope of the loop and an Action, which relates to the functionality to be executed.

Let's test this with a example that is similar, but not exact, to the previous one. We'll use the same IsPrime algorithm, but this time, we'll write the results checking one by one inside a for loop. So, we start with a simple loop that checks the first 1000 numbers and loads the result in RichTextbox.

Our initial code for the nonparallel version will be as follows:

private void btnStandardFor_Click(object sender, EventArgs e)
{
  rtbOutput.ResetText();
  watch.Start();
  for (int i = 1; i < 1000; i++)
  {
    if (i.IsPrime())
      rtbOutput.Text += string.Format("{0} is prime", i) + cr;
    else
      rtbOutput.Text += string.Format("{0} is NOT prime", i) + cr;
  }
  watch.Stop();
  label1.Text = "Elapsed Time: " + watch.ElapsedMilliseconds.ToString("0,000") + " ms."; ;
}

The problem here is knowing how to transform the previous code into a Parallel.For. Now, the action to perform by the loop is indicated by a lambda expression that is in charge of checking each value.

However, we find an extra problem. Since this is parallel and new threads will be created, we can't update the user interface directly, or we will get InvalidOperationException.

There are several solutions for this, but one of most used solutions is in the SynchronizationContext object. As Joydip Kanjilal states in Learning Synchronization Context, async, and await (refer to http://www.infoworld.com/article/2960463/application-development/my-two-cents-on-synchronizationcontext-async-and-await.html), The SynchronizationContext object represents an abstraction it denotes the location where your application's code is executed and enables you to queue a task onto another context (every thread can have its own SynchronizatonContext object). The SynchronizationContext object was added to the System.Threading namespace to facilitate communication between threads.

The resulting code for our Parallel. For that will look like this:

// Previously, at class definition:
Stopwatch watch = newStopwatch();
string cr = Environment.NewLine;
SynchronizationContext context;

public Form1()
{
  InitializeComponent();
  //context = new SynchronizationContext();
  context = SynchronizationContext.Current;
}

private void btnParallelFor_Click(object sender, EventArgs e)
{
  rtbOutput.ResetText();
  watch.Restart();
  Parallel.For(1, 1000, (i) =>
  {
    if (i.IsPrime())
      context.Post(newSendOrPostCallback((x) =>
    {
      UpdateUI(string.Format("{0} is prime", i));
    }), null);
    else
      context.Post(newSendOrPostCallback((x) =>
    {
      UpdateUI(string.Format("{0} is NOT prime", i));
    }), null);
  });
  watch.Stop();
  label2.Text = "Elapsed Time: " + watch.ElapsedMilliseconds.ToString("0,000") + " ms.";
}
private void UpdateUI(string data)
{
  this.Invoke(newAction(() =>
  {
    rtbOutput.Text += data + cr;
  }));
}

With this approach, we send a synchronization order from the thread in execution (whichever it is) to the main thread (the UI Thread). To do this, we first cache the SynchronizationContext object of the current thread at definition time, and later, we use it to call the Post method on the context which will invoke a new action to update the user interface.

Note that this solution is coded in this way to show that Parallel.For can also be used in processes that (one at a time) manipulate the user interface.

We can appreciate the difference between both approaches calculating the same primes, as shown in the next screenshot:

The Parallel class

The Parallel.ForEach version

Another variant of the same idea is Parallel.ForEach. It's practically the same except that we don't have a starting or ending number in the definition. It's better to use a sequence of information and a unique variable that we'll use to iterate over each element of the sequence.

However, I'm going to change the type of process for this demo so that you can compare and get your own conclusions. I will go through a list of small .png files (icons128 x 128), and I'll create a new version of these icons (transparent), saving the new modified icon in another directory.

We're using an IO-bound process in this case. The slow method will be linked to the disk drive, not to the CPU. Other possible IO-bound processes you could try include downloading files or images from a website or blog posts from any social network.

Since the most important thing here is time gain, we'll process the files one after the other and compare the resulting elapsed times, showing the output in a window. I'll use a button to launch the process with the following code (please, note that Directory.GetFiles should point to a directory of your own where some .png files are present):

Stopwatch watch = new Stopwatch();
string[] files = Directory.GetFiles(@"<Your Images Directory Goes Here>", "*.png");
string modDir = @"<Images Directory>/Modified";

public void ProcessImages()
{
  Directory.CreateDirectory(modDir);
  watch.Start();

  foreach (var file in files)
  {
    string filename = Path.GetFileName(file);
    var bitmap = ne0wBitmap(file);
    bitmap.MakeTransparent(Color.White);
    bitmap.Save(Path.Combine(modDir, filename));
  }
  watch.Stop();

  lblForEachStandard.Text += watch.ElapsedMilliseconds.ToString() + " ms.";
  watch.Restart();

Parallel.ForEach(files, (file) =>
  {
    string filename = Path.GetFileName(file);
    var bitmap = newBitmap(file);
    bitmap.MakeTransparent(Color.White);
    bitmap.Save(Path.Combine(modDir, "T_" + filename));
  });
  watch.Stop();
  lblParallel.Text += watch.ElapsedMilliseconds.ToString() + " ms.";
  MessageBox.Show("Finished");
}

As you can see, there are two loops. The second one also uses a file variable to iterate over the collection of files retrieved by the Directory.GetFiles() call, but the second argument of the Parallel.ForEach loop is a lambda expression, containing exactly the same code as the first foreach method (well, with the slight difference that I'm appending a T_ prefix to the name before saving it).

However, the difference in the processing time is meaningful, even in this case where just a handful of files were available (around a hundred).

You can see the difference in the next screenshot:

The Parallel.ForEach version

So, in both samples, either CPU- or IO-bound, the gain is important, and other considerations apart (there's always some), we have a nice solution here, with these two options for parallelism (Remember that you should change the program's entry point, depending on the demo to execute, in the Program.cs file).

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

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