Sunday, October 16, 2011

Task Parallel Library (TPL)–The lambda strikes back

 

Hi All,

I would like to share an interesting insight I got when solving some problems at Project Euler.  I was trying to make my program run a little faster (because it was using only one CPU core) so I decided to use the TPL. I will spare you the details of the actual problem and instead present a simple piece of code which demonstrates the problem:

  1. public static void Run()
  2.     {
  3.       Task[] tasks = new Task[10];
  4.       for (int i = 0; i < 10; i++)
  5.       {
  6.         tasks[i] = Task.Factory.StartNew(() => { PrintNumber(i); });
  7.       }
  8.  
  9.       Task.WaitAll(tasks);
  10.     }
  11.  
  12.     private static void PrintNumber(int i)
  13.     {
  14.       Thread.Sleep(100);
  15.       Console.Write(i);
  16.       Console.Write(",");
  17.     }

What do you think will be printed when I call Run?

 

If you think that this is not deterministic because the TPL may choose to run the tasks at any order you would be half right. Indeed, the TPL can run the tasks in any order but in this case we have a different problem. The evaluation of the lambda expression will be done only when the task is run and in the general case it would be after the loop is finished so the value of “i" would be 10 for all the tasks.

The correct answer is therefore that the program will print 10 times “10,” (in the general case because in some cases the task may start in the middle of the loop).

I remind you from a previous post on my blog that when you use a local variable in a lambda expression the compiler generates a “secret” class for you which holds a reference (for reference types) or a copy (for value types) of this variable until the lambda expression is run. Therefore to get the desired result we need a unique variable to be copied in each iteration. This can be achieved by adding a temp variable within the scope of the loop and use it in the lambda.

  1. public static void Run()
  2. {
  3.   Task[] tasks = new Task[10];
  4.   for (int i = 0; i < 10; i++)
  5.   {
  6.     int x = i;
  7.     tasks[i] = Task.Factory.StartNew(() => { PrintNumber(x); });
  8.   }
  9.  
  10.   Task.WaitAll(tasks);
  11. }
  12.  
  13. private static void PrintNumber(int i)
  14. {
  15.   Thread.Sleep(100);
  16.   Console.Write(i);
  17.   Console.Write(",");
  18. }

Now we get the correct results, the numbers 0 though 9 are printed (in some order).

My conclusion is (again): One should be extra careful when using local variables in lambda expressions.

 

Thanks for reading,

Boris.

No comments:

Post a Comment