BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Parallel and Asynchronous
.NET 4.0+

Using Parallel.Invoke

The seventh part of the Parallel Programming in .NET tutorial starts a look at the options that the Task Parallel Library provides for task decomposition. This article describes the Parallel.Invoke method, which can execute several delegates in parallel.

Task Parallelism

In the earlier instalments of the tutorial we looked at some of the concepts of parallel programming with the .NET framework, followed by several articles describing data decomposition and parallelism using the Parallel.For and Parallel.ForEach loops. The loops execute the same algorithm numerous times in a set of iterations. The iterations may be split into chunks and divided between multiple processors or cores to run in parallel.

Some algorithms do not lend themselves to data decomposition because they are not repeating the same action. However, they may be candidates for task decomposition. This is where an algorithm is broken into sections that can be executed independently. Each section is considered to be a separate task that may be executed on its own processor core, with several tasks running concurrently. This type of decomposition is usually more difficult to implement and sometimes requires that an algorithm be changed substantially or replaced entirely to minimise elements that must be executed sequentially and to limit shared mutable values.

Parallel.Invoke

We'll start our investigation of task parallelism in .NET with a look at the Parallel.Invoke method. This method provides a simple way in which a number of tasks may be created and executed in parallel. As with other methods in the Parallel Task Library, Parallel.Invoke provides potential parallelism. If no benefit can be gained by creating multiple threads of execution the tasks will run sequentially.

To use Parallel.Invoke, the tasks to be executed are provided as delegates. The method uses a parameter array for the delegates to allow any number of tasks to be created. The tasks are usually defined using lambda expressions but anonymous methods and simple delegates may be used instead. Once invoked, all of the tasks are executed before processing continues with the command following the Parallel.Invoke statement. The order of execution of the individual delegates is not guaranteed so you should not rely on the results of one operation being available for one that appears later in the parameter array.

To demonstrate the Parallel.Invoke method we will create a console application. The Parallel class is found in the System.Threading.Tasks namespace and we will also use the Thread class from the System.Threading namespace in the program, so add the following using directives:

using System.Threading.Tasks;
using System.Threading;

The following sample uses Parallel.Invoke to start three separate tasks. Each displays a message to show that the task has started. There is then a pause of between one and five seconds before a second message indicating the completion of the task is outputted.

Parallel.Invoke(
    () => {
        Console.WriteLine("Task 1 started");
        Thread.Sleep(5000);
        Console.WriteLine("Task 1 complete");
    },
    () => {
        Console.WriteLine("Task 2 started");
        Thread.Sleep(3000);
        Console.WriteLine("Task 2 complete");
    },
    () => {
        Console.WriteLine("Task 3 started");
        Thread.Sleep(1000);
        Console.WriteLine("Task 3 complete");
    });
 
/* OUTPUT
 
Task 2 started
Task 1 started
Task 3 started
Task 3 complete
Task 2 complete
Task 1 complete

*/

The results shown in the comment are those of one run of the program on a machine with a dual-core processor. It's interesting to see that the second task in the parameter array started before the first one in this case. You can see that all three tasks were executing in parallel even though only two cores were available. This is because the Task Parallel Library identified that processors became idle when tasks were paused by the Thread.Sleep method. This allowed the final task to be started efficiently. Note that the results vary from run to run and the order of execution can change.

Catching Exceptions from Parallel.Invoke

As we've seen previously when catching exceptions in parallel loops, exceptions may be thrown by more than one task. In the case of Parallel.Invoke, it is guaranteed that every task will be executed. Each task will either exit normally or throw an exception. All of the thrown exceptions are gathered together and held until all of the tasks have stopped, at which point an AggregateException containing all of the exceptions is thrown. The individual errors can be found within the InnerExceptions property.

In the following example we again launch three processes that may run in parallel. The first exits normally. The second attempts a division by zero so an exception is thrown. The third task throws an exception directly from the code. The Parallel.Invoke method is contained within a try / catch block which handles AggregateExceptions only, outputting the Message property of any exceptions that occur. The results show that the two exceptions that we would expect are found in the AggegateException's InnerExceptions property.

try
{
    Parallel.Invoke(
    () => {
        Console.WriteLine("Task 1 started");
        Thread.Sleep(5000);
        Console.WriteLine("Task 1 complete");
    },
    () => {
        Console.WriteLine("Task 2 started");
        int i = 0;
        Console.WriteLine(1 / i);
    },
    () => {
        Console.WriteLine("Task 3 started");
        Thread.Sleep(1000);
        throw new InvalidOperationException("Test Exception");
    });
}
catch (AggregateException ex)
{
    Console.WriteLine("\nThere were exceptions:\n");
    foreach (var exception in ex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}

/* OUTPUT

Task 1 started
Task 2 started
Task 3 started
Task 1 complete

There were exceptions:

Test Exception
Attempted to divide by zero.

*/
16 September 2011