BlackWaspTM
Parallel and Asynchronous
.NET 4.0+

Task Cancellation

The fourteenth part of the Parallel Programming in .NET tutorial examines how parallel tasks are cancelled. This includes stopping single tasks, co-ordinating the cancellation of multiple tasks and dealing with tasks that are cancelled before they start.

Cancelled Task Statuses

There is a problem with the above approach. When a task completes, several properties can be read to determine its status. When exiting the task normally, the Status property is set to RanToCompletion and the IsCanceled property is false. This suggests that the task completed normally and was not cancelled.

To see the properties, modify the Main method as shown below and run the code again. The last three messages outputted show the task's status.

static void Main()
{
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;
    var task = new Task(() => DoLongRunningTask(token), token);

    Console.WriteLine("Press Enter to cancel");
    task.Start();

    Console.ReadLine();
    tokenSource.Cancel();
    task.Wait();

    Console.WriteLine("Status:      {0}", task.Status);
    Console.WriteLine("IsCanceled:  {0}", task.IsCanceled);
    Console.WriteLine("IsCompleted: {0}", task.IsCompleted);

    task.Dispose();
    Console.ReadLine();
}

/* FINAL OUTPUT

Status:      RanToCompletion
IsCanceled:  False
IsCompleted: True

*/

OperationCanceledException

To cancel a task and correctly set the status properties you can throw an OperationCanceledException. Unlike in other scenarios, where throwing exceptions is not advisable to control the normal flow of a program, this is the supported manner to indicate cancellation of a task. Rather than checking the IsCancellationRequested property and throwing the exception manually, you can use the token's ThrowIfCancellationRequested method. As the name suggests, this only throws an exception if cancellation is required.

To use an exception to cancel the task correctly, modify the DoLongRunningTask method as follows:

static void DoLongRunningTask(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    for (int i = 0; i <= 100; i++)
    {
        Console.WriteLine("{0}%", i);
        Thread.Sleep(1000);
        token.ThrowIfCancellationRequested();
    }
}

When the task is cancelled by throwing an exception, we should capture that exception and deal with it appropriately. For the time being, add a try / catch block around the Wait call as shown below. Run the code to see that the statuses are now correct.

NB: If you forget to pass the token to the Task's constructor, throwing the exception will set the Status to Faulted, rather than Cancelled.

static void Main()
{
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;
    var task = new Task(() => DoLongRunningTask(token), token);

    Console.WriteLine("Press Enter to cancel");
    task.Start();
            
    Console.ReadLine();
    tokenSource.Cancel();

    try { task.Wait(); }
    catch { }

    Console.WriteLine("Status:      {0}", task.Status);
    Console.WriteLine("IsCanceled:  {0}", task.IsCanceled);
    Console.WriteLine("IsCompleted: {0}", task.IsCompleted);

    task.Dispose();
    Console.ReadLine();
}

/* FINAL OUTPUT

Status:      Canceled
IsCanceled:  True
IsCompleted: True

*/

Handling Cancellation Exceptions

The last problem with the code is that any real exception thrown will now be swallowed by the empty catch block. We know that an exception that happens within a task will be wrapped in an AggregateException. Potentially, there could be multiple inner exceptions of which some are OperationCancelledExceptions that should be ignored but some are other types that should be handled or rethrown.

We can quickly eliminate the OperationCanceledExceptions using the AggregateException's Handle method. This handy method executes a Func delegate for each of the inner exceptions. If the delegate returns true for an individual exception, that exception is deemed to have been handled. If all of the exceptions are flagged as handled, processing continues as normal. However, if any of the exceptions generate a false result, they are added to a new AggregateException and rethrown. We can use this to handle all of the OperationCancelledExceptions by simply checking their types. Any other type exception will be rethrown and can be handled separately.

To ignore the cancellation exceptions, modify the try / catch block as follows. Note that the lambda expression simply checks the type using the "is" keyword. After making the change, run the program again to see it operating correctly.

try
{ 
    task.Wait();
}
catch(AggregateException ex)
{
    ex.Handle(e =>
    {
        return e is OperationCanceledException;
    });
}
26 November 2011