/ asynclinqplinq

Async and PLINQ

(The code in this post is also available on Github.)

Went to the Cleveland WPF User Group last night for the first time. They have a nice room setup at DeVry University’s Seven Hills campus… wide and only a few rows deep, with two projectors and a computer setup for the presenter. No guest wi-fi access that I could get to.

Richard Broida, of Bennett Adelson, discussed Async Programming. He covered multiple methods, from the first delegates in use with .NET 1.0, to events and BackGroundWorkerThreads, to PLINQ and Reactive Extensions (both of which I want to read more about). He also covered the basics of how a computer executes multiple threads simultaneously, how to handle exceptions in threads, cancelling threads and waiting for everything to finish before continuing. It was a lot of material in a relatively short time, so unfortunately most of it was in one ear and out the other. I’m hoping I retained it in there somewhere for when I can use it. If he uploads his slides somewhere online I’ll link to them.

I decided to try something simple when I got home, so I started messing with PLINQ, namely .AsParallel() in System.Linq. This is just a quick and dirty console app, where I’m trying to see the obvious benefits of multi-threading. Here I’m operating on a list of numbers, performing a pseudo “long-running” operation on each.

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace SampleConsole
{
  class Program
  {
    private static readonly Stopwatch Stopwatch = new Stopwatch();

    static void Main()
    {
      Stopwatch.Start();
      PLinqTest();
      Stopwatch.Stop();

      Console.WriteLine("Total execution time: {0}", Stopwatch.ElapsedMilliseconds / 1000m);
      Console.Read();
    }
    static void PLinqTest()
    {
      var numbers = new[] { 1, 2, 3, 4 }; 
      numbers.AsParallel().Select(SomeLongComputation).ToList();
    }
    static int SomeLongComputation(int n)
    {
      for (var i = 0; i <= 5;i++)
      {
        Thread.Sleep(100);
        Console.WriteLine(string.Concat(n, ":", i));
      }
      return -1;
    }
  }
}

Here are the results when I ran it twice in a row. There are 4 integers, and for each one I’m doing six 100ms Thread.Sleep calls. All 4 numbers are executed on simultaneously, in random order, and the whole process takes the same time as it would take to run on 1 integer.

linqAsParallelTest1

linqAsParallelTest2

Here’s the results after I removed .AsParallel(). Each number is operated on one at a time. The total execution time is the sum of all the parts.. one thread. The order is predictable, but the time is much slower. If you needed the result of one action before starting the next, then I suppose this would be the way to go. But if you had a bunch of number-crunching or data manipulation that could be performed independently, multi-threading would be a life-saver.

linqAsParallelWithoutTest

You can limit the number of threads running by using .AsParallel().WithDegreeOfParallelism(2), which specifies the number of concurrent tasks (in this case, 2) to execute. So now I’d expect two numbers to be processed at once instead of just one or all four. The time should be between the above two.

linqAsParallelTest3

If the calculations are extremely long running, and you want to give the user a chance to cancel the process, you can specify a CancellationToken. Here’s the code again, modified to cancel the process. Granted, this is a console app, so I just setup a Timer to issue the cancellation.

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace SampleConsole
{
  class Program
  {
    private static readonly Stopwatch Stopwatch = new Stopwatch();
    private static CancellationToken cancellationToken;

    static void Main()
    {
      new Timer(x => cancellationToken = new CancellationToken(true), "hi", 250, 10000);
      Stopwatch.Start();
      PLinqTest();
      Stopwatch.Stop();
      Console.WriteLine("Total execution time: {0}", Stopwatch.ElapsedMilliseconds / 1000m);
      Console.Read();
    }

    static void PLinqTest()
    {
      var numbers = new[] { 1, 2, 3, 4 };
      numbers.AsParallel().WithCancellation(cancellationToken) .Select(SomeLongComputation).ToList();
    }

    static int SomeLongComputation(int n)
    {
      for (var i = 0; i <= 5; i++)
      {
        if (cancellationToken.IsCancellationRequested)
          break;
        Thread.Sleep(100);
        Console.WriteLine(string.Concat(n, ":", i));
      }
      return -1;
    }
  }
}

I changed the initial delay time a few times to see if the threads did indeed cancel:

250 ms:

linqCancellationToken1

350 ms:

linqCancellationToken2

450 ms:

linqCancellationToken3

You can read more about the cancellation model here:

(The code in this post is also available on Github.)


Grant Winney

Grant Winney

I write when I've got something to share - a personal project, a solution to a difficult problem, or just an idea. We learn by doing and sharing. We've all got something to contribute.

Read More