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+

Cancelling Tasks Started with Parallel.Invoke

The Parallel.Invoke method provides a simple way to execute multiple tasks in parallel, without the requirement to create Task objects directly or wait for their completion. It also permits the cancellation of the task group via cancellation tokens.

Parallel.Invoke

In the "Parallel Programming in .NET" tutorial I described the use of the Parallel.Invoke method, which allows you to execute a number of tasks with potential parallelism. The method also performs synchronisation automatically, ensuring that all of the scheduled tasks complete before execution continues with the line of code following the Invoke call.

In some situations, the results of one task started by Parallel.Invoke should cancel the other tasks. For example, if one task in a long-running group fails, it may not be necessary to start any tasks that have not yet commenced. Another possibility is that you have started multiple tasks where only one needs to be successful in order to continue with the program. In such cases, when one task succeeds, you can cancel the others to improve performance.

In this article we will create an example program that starts multiple tasks using Parallel.Invoke. We'll then add cancellation logic. To begin, create a new console application project. As we'll be using classes from the System.Threading and System.Threading.Tasks namespaces, add the following using directives:

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

Creating the Sample Program

Parallel.Invoke requires an array of Action delegates, each of which is scheduled for execution. These may then run in parallel, depending upon their functionality and the availability of multiple processors or processor cores on the target computer. We'll use two methods to generate this Action array. The LongTask method outputs two messages, pausing between them. CreateTaskArray creates an array of Action delegates, each of which calls LongTask.

private static Action[] CreateTaskArray(int taskCount)
{
    var actions = new Action[taskCount];
    for (int i = 0; i < taskCount; i++)
    {
        actions[i] = LongTask;
    }
    return actions;
}

static void LongTask()
{
    Console.WriteLine("Starting {0}", Task.CurrentId);
    Thread.Sleep(1000);
    Console.WriteLine("Completed {0}", Task.CurrentId);
}

We can now add code that creates and invokes some tasks. The following code, added to the Main method, creates five delegates and executes them. The comments show a possible output. This was generated on a dual-core machine. You can see the parallelism from the messages. The first three tasks execute concurrently. Tasks four and five commence only after the first two tasks exit.

var actions = CreateTaskArray(5);
Parallel.Invoke(actions);

/* OUTPUT

Starting 1
Starting 2
Starting 3
Completed 2
Completed 1
Starting 4
Starting 5
Completed 3
Completed 4
Completed 5

*/

Cancelling Parallel.Invoke Tasks

Cancellation of tasks started using Parallel.Invoke uses the same cancellation token approach as when cancelling other parallel operations. Before you start the parallel execution, you must create a CancellationTokenSource object, from which you can request a CancellationToken. This token is passed to the Parallel.Invoke method's first parameter, with the Action array becoming the second parameter. However, the token cannot be provided directly as when creating cancellable Task objects. Instead, you must instantiate a ParallelOptions object and set its CancellationToken property to the generated token.

With the cancellation token provided, you can call its Cancel method from within any of the Action delegates. To avoid problems with early termination, cancelling the tasks does not immediately stop their execution. Any tasks from the array that have not already begun executing will not be started. Any tasks that have already started will continue to run until they complete normally or throw an exception. For particularly long-running tasks, you may also check the cancellation token source's IsCancellationRequested property. If this is true, you can gracefully exit from a task early to improve performance.

When the tasks are cancelled, an OperationCanceledException is thrown, as it would be when you cancel tasks manually. You should generally catch this exception to ensure that it does not cause your software to exit abnormally. Other exceptions will be wrapped in an AggregateException and should be handled appropriately.

To demonstrate cancellation, first we need a cancellation token source that is visible to the entire program. Add the following declaration to the class, outside of any member.

static CancellationTokenSource _tokenSource;

We'll also add a new method that cancels the operation. This will be called by one of the Action delegates.

static void CancellingTask()
{
    Console.WriteLine("Cancelling {0}", Task.CurrentId);
    _tokenSource.Cancel();
}

Finally, we need to update the Main method to contain the code shown below. This instantiates the cancellation token source and obtains a token, which is wrapped in a ParallelOptions object. The code then creates an array of five Actions and sets the third to call the method that cancels the entire parallel operation. The Invoke call is updated to pass the ParallelOptions object.

You can see from the results in the comments that, in this case, task one completed before the cancellation action was executed. After the cancellation, task two ran to completion but tasks four and five were never started.

_tokenSource = new CancellationTokenSource();
var options = new ParallelOptions { CancellationToken = _tokenSource.Token };
var actions = CreateTaskArray(5);
actions[2] = CancellingTask;

try { Parallel.Invoke(options, actions); }
catch (OperationCanceledException) { Console.WriteLine("Cancelled"); }

/* OUTPUT

Starting 1
Starting 2
Completed 1
Cancelling 3
Completed 2
Cancelled

*/
2 July 2012