Parallel.Invoke() vs. Explicit Task Management

Parallel.Invoke() vs. Explicit Task Management

  • Comments 10

 

 

Parallel Extensions offers a large variety of APIs supporting parallelism.

During this blog the discussion will be focused on the methodology for making a choice between two of the new Parallel Extensions concepts: parallelism achieved by using Parallel.Invoke() and parallelism achieved through the use of Tasks.

 

Suppose that you wanted the two actions below to be executed in parallel:

 

Action hello = () => { Console.Write("Hello"); };

Action world = () => { Console.Write("World"); };

 

Should you use Tasks or Parallel.Invoke() to achieve the desired parallelism?

Parallelism through Parallel.Invoke()

The code to execute our actions in parallel with Parallel.Invoke() looks like this:

 

Parallel.Invoke(hello, world);

 

This is simple and easy, and we get the desired result: either “HelloWorld” or “WorldHello” output to the console, depending on the order in which the parallel actions were scheduled.

Parallelism through Explicit Task Management

For convenience, creating and starting a Task can be done with a single method:

 

Task.Factory.StartNew(hello);

Task.Factory.StartNew(world);

 

If we run this code sequence, something surprising happens – nothing is displayed! The reason is that the Tasks are scheduled via ThreadPool threads, which are background threads.  The main process will execute while the Tasks are still running.  At the same time, if one of the actions throws an exception, the exception will not be thrown until the Finalizer throws it. We can solve both of these problems by performing an explicit Wait() on the Tasks:

 

Task taskHello = Task.Factory.StartNew(hello);

      Task taskWorld = Task.Factory.StartNew(world);

      Task.WaitAll(taskHello, taskWorld);

 

Running this sample now, the result is the same as in the Parallel.Invoke case:  either “HelloWorld” or “WorldHello” is printed to the console.

An Example: Tree Traversal

Let’s see now how we can implement a binary tree traversal using Parallel.Invoke():

 

ConcurrentBag<int> _dataStorage = new ConcurrentBag<int>();

 

void TreeTraversal(Tree<int> node)

{

    if (node == null)

        return;

   

    var actions = new List<Action>();

    if(node._left != null)

        actions.Add(() => TreeTraversal(node._left));

    if(node._right != null)

        actions.Add(() => TreeTraversal(node._right));

 

    Parallel.Invoke(actions.ToArray());

    _dataStorage.Add(node._data);

}

 

And let’s look at a similar version using explicit Task management:

 

ConcurrentBag<int> _dataStorage = new ConcurrentBag<int>();

 

void TreeTraversal(Tree<int> node)

{

    if (node == null)

        return;

 

    var tasks = new List<Task>();

    if(node._left != null)

        tasks.Add(Task.Factory.StartNew(

            () => TreeTraversal(node._left));

    if(node._right != null)

        tasks.Add(Task.Factory.StartNew(

            () => TreeTraversal(node._right));

   

    _dataStorage.Add(node._data);

   

    Task.WaitAll(tasks.ToArray());

}

 

Note that the syntax is more concise in the Parallel.Invoke() version, and the Parallel.Invoke() call takes care of waiting and exception handling.  The flip side of that is that the explicit task management version allows for more control.  Note that the “_dataStorage.Add(node._data);” operation can be performed in parallel with the processing of the left and right branches when explicit task management is used, while the same operation must wait for left/right processing to complete in the Parallel.Invoke() version.

Conclusions

Parallel.Invoke() is a higher-level mechanism for providing parallelism, and allows for more concise code that one would typically get from using explicit task management. 

However, if the coder is interested in more control, perhaps for more complicated scenarios, then explicit task management is probably the way to go.

Leave a Comment
  • Please add 4 and 8 and type the answer here:
  • Post
  • Can you also explain the difference in how the parallel tasks may be canceled? For example, in hello world example, how would developer inform the Parallel.Invoke() that all pending tasks should be canceled, but those already executing should finish? What about the Task example?

    Thanks!

  • [added several corrections for Beta1]

    Hello Yuri,

    Cancellation of Parallel Loops is supported through the concepts of CancellationTokenSource and CancellationToken. A cancellationToken can be passed to a ParallelFor through ParallelOptions.

    In order to cancel a task (or more), Cancel method can be used on a task.

    More info on cancellation please see at: http://blogs.msdn.com/pfxteam/archive/2009/06/22/9791840.aspx

    For your specific question I added below some source code with comments that, I hope will help.

    It addresses both questions: cancel a ParalleInvoke and cancel bunch of tasks.

    Thanks,

    Cristina

          static CountdownEvent _ctd ;

          public static void ParallelInvokeSample()

          {

              int n = System.Environment.ProcessorCount;

              _ctd = new CountdownEvent(System.Environment.ProcessorCount);

              //create an array of n + 1 actions

              Action[] allActions = CreateActions(n + 10);

              CancellationTokenSource cts = new CancellationTokenSource();

              ParallelOptions pOptions = new ParallelOptions() { CancellationToken = cts.Token };

              //on a different thread cancel the token by calling cancel on the tokenSource

              Thread cancel = new Thread(() =>

                  {

                      //wait for the parallel invocation to start

                      _ctd.Wait();

                      cts.Cancel();

                  });

              cancel.Start();

              try

              {

                  //pass the token to ParallelInvoke through parallelOptions

                  Parallel.Invoke(pOptions, allActions);

              }

              catch( Exception e )

              {

                  Console.WriteLine(e.Message);

              }

              cancel.Join();

          }

          public static void TasksSample()

          {

    int n = System.Environment.ProcessorCount;

               _ctd = new CountdownEvent(System.Environment.ProcessorCount);

               try

               {

                   //first create a factory with RespectParentCancellation option

    TaskFactory factory = new TaskFactory(TaskCreationOptions.RespectParentCancellation,TaskContinuationOptions.None);

                   //create a number of tasks that will be cancelled through a common parent

                   Task[] allTasks = new Task[n + 10];

                   Task parent = Task.Factory.StartNew(() =>

                       {

                           for (int m = 0; m < n + 10; m++)

                           {

                               allTasks[m] = factory.StartNew(() =>

                               {

                                   Console.WriteLine("hello");

                                   try { _ctd.Signal(); }

                                   catch

                                   {

                                       //ignore any exception

                                   }

                                   //do some long running work

                                   for (int k = 0; k < 1000; k++)

                                   {

                                       Thread.Sleep(2);

                                   }

                               });

                           }

                       });

                   //wait for some tasks to start

                   _ctd.Wait();

                   //cancell all the tasks, by cancelling the parent

                   parent.Cancel();

                   //wait on the tasks

                   Task.WaitAll(allTasks);

               }

               catch (AggregateException e)

               {

                   Console.WriteLine(e.InnerExceptions.Select((x) => x.Message).Aggregate((x, y) => { return x + y; }));

               }

          }

          static Action[] CreateActions(int n)

          {

              Action[] result = new Action[n];

              for (int i = 0; i < n; i++)

              {

                  result[i] = () =>

                  {

                      Console.WriteLine("hello");

                      try { _ctd.Signal(); }

                      catch

                      {

                          //ignore any exception

                      }

                      //do some long running work

                      for (int k = 0; k < 1000; k++)

                      {

                          Thread.Sleep(2);

                      }

                  };

              }

              return result;

          }

  • Couple of quick questions:

    a) What is the performance impact (if any) of not having a "flat" task hierarchy. For example, if thread A is the main thread, what if we have:

    A --> B, C and B --> D,E vs.

    A --> C, D, E,

    The latter obfuscates code more, but there are less tasks created. Is the "performance" significant, or is this taken care of "under the covers"

    b) For the Tree Traversal code, you could have just added the actions without checking whether the left or right tree was null and it would still be functionally correct. Is there a significant performance trade-off between more concise code and doing this check before scheduling tasks?

  • Hello Sachin

    For question “b”, without the checking that you mention, we would pay the cost of creating/scheduling new tasks (this means possibly creating new threads)  without really needing them. So. I would say that. if possible to do some un-expensive validations, it is recommended to do them before creating new tasks.

    For question “a”,  if I understand correctly your scenario, the 3 tasks (C, D E ) need to be scheduled. They can be scheduled either directly ( a.2), or through the task B ( a.1). I do not see a reason for introducing task B, if it is not needed. As above, any new task created might lead to a new thread injection. Also, with a new task, probably it will be needed to wait on it ( implicitly or explicitly) - this will add a performance penalty as well. In conclusion, I would recommend to try to not create tasks as long as they are not needed.  Parallel.Invoke takes care of creating the right number of tasks for executing all the actions.

    Also, because you may be interested in profiling parallel applications, I think that  you will find useful the new parallel profiling tools that  will be shipped with Visual Studio 2010. (http://blogs.msdn.com/hshafi/archive/2009/05/18/visual-studio-2010-beta-1-parallel-performance-tools.aspx,.  http://blogs.msdn.com/hshafi/archive/2009/06/02/vs2010-how-to-use-the-parallel-performance-analysis-tools.aspx)

  • Please, can you explain how it is with exception handling for both priciples. Imagine situation when the method TreeTraversal() fire exception. I expect, that some exception will be thrown from 'Task.WaitAll(tasks.ToArray())' or 'Parallel.Invoke(actions.ToArray())'. But the question is which expection wins when variable 'tasks' or 'actions' contains more than one task. Or all exceptions thrown in TreeTraversal() will be silently 'consumed' by framework?

    For tasks I know that I can review task.Exception property to see if this task raises any exception, but which exception will be thrown from Task.WaitAll? And how it is for Parallel.Invoke?

    Thanks Michal

  • Hi Michal-

    A general principle followed by the parallelism technologies in .NET 4 is that we never want to lose or eat exceptions. As such, a new exception type System.AggregateException has been introduced in .NET 4, which is an Exception-derived type that supports containing multiple inner exceptions rather than just a single inner exception.  In the cases you ask about, if any of the tasks passed to Task.WaitAll complete in a faulted state, all of their exceptions will be wrapped in an AggregateException instance, and that aggregate exception will be thrown, thus allowing you to see all of the exceptions.  Same goes for Parallel.Invoke: if multiple actions failed, Invoke will throw an aggregate containing all of the exceptions.

    More information on this is available in a new article just posted in the latest issue of MSDN Magazine: http://msdn.microsoft.com/en-us/magazine/ee321571.aspx.

  • hello, can you help me about DAG parallel programing withe TPL

    / DAG (Directed Acyclic Graph)

    thank's

  • What is it you're having trouble with?

  • thank you in advance sir ...

    if you have any idea about the transformation code to DAG.

     and parallel exucution of  DAG (how does it).

  • That's still a very broad topic / question.  I'd suggest reading the section on fork/join in paper at www.microsoft.com/.../details.aspx and see if that helps.  There's also a small section there specifically about Directed Acyclic Graphs, which you can search the PDF for.

Page 1 of 1 (10 items)