Introducing the Task Parallel Library in Microsoft Visual C# 2010

  • 4/15/2010

Canceling Tasks and Handling Exceptions

Another common requirement of applications that perform long-running operations is the ability to stop those operations if necessary. However, you should not simply abort a task because this could leave the data in your application in an indeterminate state. Instead, the TPL implements a cooperative cancellation strategy. Cooperative cancellation enables a task to select a convenient point at which to stop processing and also enables it to undo any work it has performed prior to cancellation if necessary.

The Mechanics of Cooperative Cancellation

Cooperative cancellation is based on the notion of a cancellation token. A cancellation token is a structure that represents a request to cancel one or more tasks. The method that a task runs should include a System.Threading.CancellationToken parameter. An application that wants to cancel the task sets the Boolean IsCancellationRequested property of this parameter to true. The method running in the task can query this property at various points during its processing. If this property is set to true at any point, it knows that the application has requested that the task be canceled. Also, the method knows what work it has done so far, so it can undo any changes if necessary and then finish. Alternatively, the method can simply ignore the request and continue running if it does not want to cancel the task.

An application obtains a CancellationToken by creating a System.Threading.CancellationTokenSource object and querying the Token property of this object. The application can then pass this CancellationToken object as a parameter to any methods started by tasks that the application creates and runs. If the application needs to cancel the tasks, it calls the Cancel method of the CancellationTokenSource object. This method sets the IsCancellationRequested property of the CancellationToken passed to all the tasks.

The following code example shows how to create a cancellation token and use it to cancel a task. The initiateTasks method instantiates the cancellationTokenSource variable and obtains a reference to the CancellationToken object available through this variable. The code then creates and runs a task that executes the doWork method. Later on, the code calls the Cancel method of the cancellation token source, which sets the cancellation token. The doWork method queries the IsCancellationRequested property of the cancellation token. If the property is set the method terminates; otherwise, it continues running.

public class MyApplication
{
    ...
    // Method that creates and manages a task
    private void initiateTasks()
    {
        // Create the cancellation token source and obtain a cancellation token
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        CancellationToken cancellationToken = cancellationToken.Token;
        // Create a task and start it running the doWork method
        Task myTask = Task.Factory.StartNew(() => doWork(cancellationToken));
        ...
        if (...)
        {
            // Cancel the task
            cancellationTokenSource.Cancel();
        }
        ...
   }

   // Method run by the task
   private void doWork(CancellationToken token)
   {
       ...
       // If the application has set the cancellation token, finish processing
       if (token.IsCancellationRequested)
       {
            // Tidy up and finish
           ...
           return;
       }
       // If the task has not been canceled, continue running as normal
       ...
   }
}

As well as providing a high degree of control over the cancellation processing, this approach is scalable across any number of tasks. You can start multiple tasks and pass the same CancellationToken object to each of them. If you call Cancel on the CancellationTokenSource object, each task will see that the IsCancellationRequested property has been set and can react accordingly.

You can also register a callback method with the cancellation token by using the Register method. When an application invokes the Cancel method of the corresponding CancellationTokenSource object, this callback runs. However, you cannot guarantee when this method executes; it might be before or after the tasks have performed their own cancellation processing, or even during that process.

...
cancellationToken,Register(doAdditionalWork);
...
private void doAdditionalWork()
{
    // Perform additional cancellation processing
}

In the next exercise, you will add cancellation functionality to the GraphDemo application.

Add cancellation functionality to the GraphDemo application

  1. Using Visual Studio 2010, open the GraphDemo solution, located in the \Microsoft Press\Visual CSharp Step By Step\Chapter 27\GraphDemo Canceling Tasks folder in your Documents folder.

    This is a completed copy of the GraphDemo application from the previous exercise that uses tasks and threads to improve responsiveness.

  2. In Solution Explorer, in the GraphDemo project, double-click GraphWindow.xaml to display the form in the Design View window.

  3. From the Toolbox, add a Button control to the form under the duration label. Align the button horizontally with the plotButton button. In the Properties window, change the Name property of the new button to cancelButton, and change the Content property to Cancel.

    The amended form should look like the following image.

  4. Double-click the Cancel button to create a Click event handling method called cancelButton_Click.

  5. In the GraphWindow.xaml.cs file, locate the getDataForGraph method. This method creates the tasks used by the application and waits for them to complete. Move the declaration of the Task variables to the class level for the GraphWindow class as shown in bold in the following code, and then modify the getDataForGraph method to instantiate these variables:

    public partial class GraphWindow : Window
    {
         ...
         private Task first, second, third, fourth;
         ...
         private byte[] getDataForGraph(int dataSize)
        {
            byte[] data = new byte[dataSize];
            first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth /
    8));
            second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
    pixelWidth / 4));
            third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
    pixelWidth * 3 / 8));
            fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 /
    8, pixelWidth / 2));
            Task.WaitAll(first, second, third, fourth);
            return data;
       }
    }
  6. Add the following using statement to the list at the top of the file:

    using System.Threading;

    The types used by cooperative cancellation live in this namespace.

  7. Add a CancellationTokenSource member called tokenSource to the GraphWindow class, and initialize it to null, as shown here in bold:

    public class GraphWindow : Window
    {
        ...
        private Task first, second, third, fourth;
        private CancellationTokenSource tokenSource = null;
        ...
    }
  8. Find the generateGraphData method, and add a CancellationToken parameter called token to the method definition:

    private void generateGraphData(byte[] data, int partitionStart, int partitionEnd,
    CancellationToken token)
    {
         ...
    }
  9. In the generateGraphData method, at the start of the inner for loop, add the code shown next in bold to check whether cancellation has been requested. If so, return from the method; otherwise, continue calculating values and plotting the graph.

    private void generateGraphData(byte[] data, int partitionStart, int partitionEnd,
    CancellationToken token)
    {
        int a = pixelWidth / 2;
        int b = a * a;
        int c = pixelHeight / 2;
    
        for (int x = partitionStart; x < partitionEnd; x ++)
        {
            int s = x * x;
            double p = Math.Sqrt(b - s);
            for (double i = -p; i < p; i += 3)
            {
                if (token.IsCancellationRequested)
                {
                    return;
                }
                double r = Math.Sqrt(s + i * i) / a;
                double q = (r - 1) * Math.Sin(24 * r);
                double y = i / 3 + (q * c);
                plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
                plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
           }
       }
    }
  10. In the getDataForGraph method, add the following statements shown in bold that instantiate the tokenSource variable and retrieve the CancellationToken object into a variable called token:

    private byte[] getDataForGraph(int dataSize)
    {
        byte[] data = new byte[dataSize];
        tokenSource = new CancellationTokenSource();
        CancellationToken token = tokenSource.Token;
        ...
    }
  11. Modify the statements that create and run the four tasks, and pass the token variable as the final parameter to the generateGraphData method:

    first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8,
    token));
    second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
    pixelWidth / 4, token));
    third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4, pixelWidth
    * 3 / 8, token));
    fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 / 8,
    pixelWidth / 2, token));
  12. In the cancelButton_Click method, add the code shown here in bold:

    private void cancelButton_Click(object sender, RoutedEventArgs e)
    {
        if (tokenSource != null)
        {
            tokenSource.Cancel();
        }
    }

    This code checks that the tokenSource variable has been instantiated; if it has been, the code invokes the Cancel method on this variable.

  13. On the Debug menu, click Start Without Debugging to build and run the application.

  14. In the GraphDemo window, click Plot Graph, and verify that the graph appears as it did before.

  15. Click Plot Graph again, and then quickly click Cancel.

    If you are quick and click Cancel before the data for the graph is generated, this action causes the methods being run by the tasks to return. The data is not complete, so the graph appears with holes, as shown in the following figure. (The size of the holes depends on how quickly you clicked Cancel.)

  16. Close the GraphDemo window, and return to Visual Studio.

You can determine whether a task completed or was canceled by examining the Status property of the Task object. The Status property contains a value from the System.Threading.Tasks.TaskStatus enumeration. The following list describes some of the status values that you might commonly encounter (there are others):

  • Created This is the initial state of a task. It has been created but has not yet been scheduled to run.

  • WaitingToRun The task has been scheduled but has not yet started to run.

  • Running The task is currently being executed by a thread.

  • RanToCompletion The task completed successfully without any unhandled exceptions.

  • Canceled The task was canceled before it could start running, or it acknowledged cancellation and completed without throwing an exception.

  • Faulted The task terminated because of an exception.

In the next exercise, you will attempt to report the status of each task so that you can see when they have completed or have been canceled.

Display the status of each task

  1. In Visual Studio, in the Code and Text Editor window, find the getDataForGraph method.

  2. Add the following code shown in bold to this method. These statements generate a string that contains the status of each task after they have finished running, and they display a message box containing this string.

    private byte[] getDataForGraph(int dataSize)
    {
        ...
        Task.WaitAll(first, second, third, fourth);
    
        String message = String.Format("Status of tasks is {0}, {1}, {2}, {3}",
            first.Status, second.Status, third.Status, fourth.Status);
        MessageBox.Show(message);
    
        return data;
    }
  3. On the Debug menu, click Start Without Debugging.

  4. In the GraphDemo window, click Plot Graph but do not click Cancel. Verify that the following message box appears, which reports that the status of the tasks is RanToCompletion (four times), and then click OK. Note that the graph appears only after you have clicked OK.

    httpatomoreillycomsourcemspimages1374372.png
  5. In the GraphDemo window, click Plot Graph again and then quickly click Cancel.

    Surprisingly, the message box that appears still reports the status of each task as RanToCompletion, even though the graph appears with holes. This is because although you sent a cancellation request to each task by using the cancellation token, the methods they were running simply returned. The .NET Framework runtime does not know whether the tasks were actually canceled or whether they were allowed to run to completion and simply ignored the cancellation requests.

  6. Close the GraphDemo window, and return to Visual Studio.

So how do you indicate that a task has been canceled rather than allowed to run to completion? The answer lies in the CancellationToken object passed as a parameter to the method that the task is running. The CancellationToken class provides a method called ThrowIfCancellationRequested. This method tests the IsCancellationRequested property of a cancellation token; if it is true, the method throws an OperationCanceledException exception and aborts the method that the task is running.

The application that started the thread should be prepared to catch and handle this exception, but this leads to another question. If a task terminates by throwing an exception, it actually reverts to the Faulted state. This is true, even if the exception is an OperationCanceledException exception. A task enters the Canceled state only if it is canceled without throwing an exception. So how does a task throw an OperationCanceledException without it being treated as an exception?

The answer lies in the task itself. For a task to recognize that an OperationCanceledException is the result of canceling the task in a controlled manner and not just an exception caused by other circumstances, it has to know that the operation has actually been canceled. It can do this only if it can examine the cancellation token. You passed this token as a parameter to the method run by the task, but the task does not actually look at any of these parameters. (It considers them to be the business of the method and is not concerned with them.) Instead, you specify the cancellation token when you create the task, either as a parameter to the Task constructor or as a parameter to the StartNew method of the TaskFactory object you are using to create and run tasks. The following code shows an example based on the GraphDemo application. Notice how the token parameter is passed to the generateGraphData method (as before), but also as a separate parameter to the StartNew method:

Task first = null;
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
...
first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8, token),
token);

Now when the method being run by the task throws an OperationCanceledException exception, the infrastructure behind the task examines the CancellationToken. If it indicates that the task has been canceled, the infrastructure handles the OperationCanceledException exception, acknowledges the cancelation, and sets the status of the task to Canceled. The infrastructure then throws a TaskCanceledException, which your application should be prepared to catch. This is what you will do in the next exercise, but before you do that you need to learn a little more about how tasks raise exceptions and how you should handle them.

Handling Task Exceptions by Using the AggregateException Class

You have seen throughout this book that exception handling is an important element in any commercial application. The exception handling constructs you have met so far are straightforward to use, and if you use them carefully it is a simple matter to trap an exception and determine which piece of code raised it. However, when you start dividing work into multiple concurrent tasks, tracking and handling exceptions becomes a more complex problem. The issue is that different tasks might each generate their own exceptions, and you need a way to catch and handle multiple exceptions that might be thrown concurrently. This is where the AggregateException class comes in.

An AggregateException acts as a wrapper for a collection of exceptions. Each of the exceptions in the collection might be thrown by different tasks. In your application, you can catch the AggregateException exception and then iterate through this collection and perform any necessary processing. To help you, the AggregateException class provides the Handle method. The Handle method takes a Func<Exception, bool> delegate that references a method. The referenced method takes an Exception object as its parameter and returns a Boolean value. When you call Handle, the referenced method runs for each exception in the collection in the AggregateException object. The referenced method can examine the exception and take the appropriate action. If the referenced method handles the exception, it should return true. If not, it should return false. When the Handle method completes, any unhandled exceptions are bundled together into a new AggregateException and this exception is thrown; a subsequent outer exception handler can then catch this exception and process it.

In the next exercise, you will see how to catch an AggregateException and use it to handle the TaskCanceledException exception thrown when a task is canceled.

Acknowledge cancellation, and handle the AggregateException exception

  1. In Visual Studio, display the GraphWindow.xaml file in the Design View window.

  2. From the Toolbox, add a Label control to the form underneath the cancelButton button. Align the left edge of the Label control with the left edge of the cancelButton button.

  3. Using the Properties window, change the Name property of the Label control to status, and remove the value in the Content property.

  4. Return to the Code and Text Editor window displaying the GraphWindow.xaml.cs file, and add the following method below the getDataForGraph method:

    private bool handleException(Exception e)
    {
        if (e is TaskCanceledException)
        {
            plotButton.Dispatcher.Invoke(new Action(() =>
            {
                status.Content = "Tasks Canceled";
            }), DispatcherPriority.ApplicationIdle);
            return true;
        }
        else
        {
            return false;
        }
    }

    This method examines the Exception object passed in as a parameter; if it is a TaskCanceledException object, the method displays the text “Tasks Canceled” in the status label on the form and returns true to indicate that it has handled the exception; otherwise, it returns false.

  5. In the getDataForGraph method, modify the statements that create and run the tasks and specify the CancellationToken object as the second parameter to the StartNew method, as shown in bold in the following code:

    private byte[] getDataForGraph(int dataSize)
    {
        byte[] data = new byte[dataSize];
        tokenSource = new CancellationTokenSource();
        CancellationToken token = tokenSource.Token;
    
        ...
        first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8,
    token), token);
        second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
    pixelWidth / 4, token), token);
        third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
    pixelWidth * 3 / 8, token), token);
        fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 / 8,
    pixelWidth / 2, token), token);
        Task.WaitAll(first, second, third, fourth);
        ...
    }
  6. Add a try block around the statements that create and run the tasks, and wait for them to complete. If the wait is successful, display the text “Tasks Completed” in the status label on the form by using the Dispatcher.Invoke method. Add a catch block that handles the AggregateException exception. In this exception handler, call the Handle method of the AggregateException object and pass a reference to the handleException method. The code shown next in bold highlights the changes you should make:

    private byte[] getDataForGraph(int dataSize)
    {
        byte[] data = new byte[dataSize];
        tokenSource = new CancellationTokenSource();
        CancellationToken token = tokenSource.Token;
    
        try
          {
            first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8,
    token), token);
            second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
    pixelWidth / 4, token), token);
            third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
    pixelWidth * 3 / 8, token), token);
            fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 /
    8, pixelWidth / 2, token), token);
            Task.WaitAll(first, second, third, fourth);
               plotButton.Dispatcher.Invoke(new Action(() =>
               {
                   status.Content = "Tasks Completed";
               }), DispatcherPriority.ApplicationIdle);
          }
          catch (AggregateException ae)
          {
              ae.Handle(handleException);
          }
    
          String message = String.Format("Status of tasks is {0}, {1}, {2}, {3}",
              first.Status, second.Status, third.Status, fourth.Status);
          MessageBox.Show(message);
    
          return data;
    }
  7. In the generateDataForGraph method, replace the if statement that examines the IsCancellationProperty of the CancellationToken object with code that calls the ThrowIfCancellationRequested method, as shown here in bold:

    private void generateDataForGraph(byte[] data, int partitionStart, int partitionEnd,
    CancellationToken token)
    {
       ...
        for (int x = partitionStart; x < partitionEnd; x++);
        {
            ...
            for (double i = -p; I < p; i += 3)
            {
                token.ThrowIfCancellationRequested();
                ...
            }
         }
         ...
    }
  8. On the Debug menu, click Start Without Debugging.

  9. In the Graph Demo window, click Plot Graph and verify that the status of every task is reported as RanToCompletion, the graph is generated, and the status label displays the message “Tasks Completed”.

  10. Click Plot Graph again, and then quickly click Cancel. If you are quick, the status of one or more tasks should be reported as Canceled, the status label should display the text “Tasks Canceled”, and the graph should be displayed with holes. If you are not quick enough, repeat this step to try again!

  11. Close the Graph Demo window, and return to Visual Studio.

Using Continuations with Canceled and Faulted Tasks

If you need to perform additional work when a task is canceled or raises an unhandled exception, remember that you can use the ContinueWith method with the appropriate TaskContinuationOptions value. For example, the following code creates a task that runs the method doWork. If the task is canceled, the ContinueWith method specifies that another task should be created and run the method doCancellationWork. This method can perform some simple logging or tidying up. If the task is not canceled, the continuation does not run.

Task task = new Task(doWork);
task.ContinueWith(doCancellationWork, TaskContinuationOptions.OnlyOnCanceled);
task.Start();
...
private void doWork()
{
    // The task runs this code when it is started
    ...
}
...
private void doCancellationWork(Task task)
{
    // The task runs this code when doWork completes
    ...
}

Similarly, you can specify the value TaskContinuationOptions.OnlyOnFaulted to specify a continuation that runs if the original method run by the task raises an unhandled exception.

In this chapter, you learned why it is important to write applications that can scale across multiple processors and processor cores. You saw how to use the Task Parallel Library to run operations in parallel, and how to synchronize concurrent operations and wait for them to complete. You learned how to use the Parallel class to parallelize some common programming constructs, and you also saw when it is inappropriate to parallelize code. You used tasks and threads together in a graphical user interface to improve responsiveness and throughput, and you saw how to cancel tasks in a clean and controlled manner.

  • If you want to continue to the next chapter

    Keep Visual Studio 2010 running, and turn to Chapter 28.

  • If you want to exit Visual Studio 2010 now

    On the File menu, click Exit. If you see a Save dialog box, click Yes and save the project.