Parallel Programming: Task Cancellation

In this post, which is the third one in my parallel programming introduction series, I want to show how you can cancel parallel operations when working with the Task Parallel Library (TPL). I’m going to modify the program that I started in the previous posts. By the way, here’s the full list of posts in this series:

At the end of the last post, I had a small parallel application with responsive UI that could be easily used in both WPF and Windows Forms UI programming models. I’m going to stick with the WPF version and add a Cancel button to the application.

This is the code I’m going to work with:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    public static double SumRootN(int root)
    {
        double result = 0;
        for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result;
    }
    private void start_Click(object sender, RoutedEventArgs e)
    {
        textBlock1.Text = "";
        label1.Content = "Milliseconds: ";

        var watch = Stopwatch.StartNew();
        List<Task> tasks = new List<Task>();
        var ui = TaskScheduler.FromCurrentSynchronizationContext();
        for (int i = 2; i < 20; i++)
        {
            int j = i;
            var compute = Task.Factory.StartNew(() =>
            {
                return SumRootN(j);
            });
            tasks.Add(compute);

            var display = compute.ContinueWith(resultTask =>
                              textBlock1.Text += "root " + j.ToString() + " " +
                                                  compute.Result.ToString() +
                                                  Environment.NewLine,
                              ui);
        }

        Task.Factory.ContinueWhenAll(tasks.ToArray(),
            result =>
            {
                var time = watch.ElapsedMilliseconds;
                label1.Content += time.ToString();
            }, CancellationToken.None, TaskContinuationOptions.None, ui);
    }

    private void cancel_Click(object sender, RoutedEventArgs e)
    {

    }

Now let’s refer to the Task Cancellation topic on MSDN. It tells me that to cancel a task I need to pass a special cancellation token to the task factory. By the way, the cancellation model is one of the new features in .NET Framework 4, so if you haven’t heard about it yet, take a look at Cancellation on MSDN and the .NET 4 Cancellation Framework post from Mike Liddell.

I have several tasks created in a loop. I’m going to use just one cancellation token so I can cancel them all at once.

I’ll add the following field to the MainWindow class:

CancellationTokenSource tokenSource = new CancellationTokenSource();

The tasks I want to cancel are the ones that compute the results (and I don’t want to cancel the tasks that display the results). So here’s the next change. The code

var compute = Task.Factory.StartNew(() =>
{
    return SumRootN(j);
});

becomes

var compute = Task.Factory.StartNew(() =>
{
    return SumRootN(j);
}, tokenSource.Token);

Finally, I’ll add code to the event handler for the Cancel button. I’ll simply call the Cancel method for the cancellation token and all tasks created with this token are notified about the cancellation. I also want to print “Cancel” into the text block.

private void cancel_Click(object sender, RoutedEventArgs e)
{
    tokenSource.Cancel();
    textBlock1.Text += "Cancel" + Environment.NewLine;
}

Let’s press F5 to compile and run the code. Now I can click Start, then click Cancel, and get an exception. (Aren’t you getting used to it?) AggregateException is the exception that tasks throw if something goes wrong. True to its name, it aggregates all the task failures into a single exception.

In my case, the tasks that calculate the results were canceled successfully, but the ones that display the results to the UI failed. This is reasonable: there were no results to display.

Let’s take a short pause here. Basically, to cancel a task, it’s enough to just pass a cancellation token to the task and then call the Cancel method on the token, as I did above. But in the real world, even in a small application like this one, you have to deal with the consequences of task cancellation.

In my example, there are several options for handling this exception. First is the standard try-catch block. The exception is thrown by the delegate within the ContinueWith method, so I need to add the try-catch block right into the lambda expression. I may get more than one exception aggregated, so I need to iterate through the collection of inner exceptions to see the exceptions’ messages.

var display = compute.ContinueWith(resultTask =>
{
    try
    {
        textBlock1.Text += "root " + j.ToString() + " " +
                            compute.Result.ToString() +
                            Environment.NewLine;
    }
    catch (AggregateException ae)
    {
        foreach (var inner in ae.InnerExceptions)
            textBlock1.Text += "root " + j.ToString() + " "
                + inner.Message + Environment.NewLine;
    }
}, ui);

This works fine. (You can compile and check, if you want to.) But once again, TPL provides a more elegant way of dealing with this type of issue: I can analyze what happened with the compute task and use its status to decide whether I want to start the display task.

var displayResults = compute.ContinueWith(resultTask =>
                     textBlock1.Text += "root " + j.ToString() + " " +
                                            compute.Result.ToString() +
                                            Environment.NewLine,
                         CancellationToken.None,
                         TaskContinuationOptions.OnlyOnRanToCompletion,
                         ui);

Now I’m passing two more parameters to the ContinueWith method. The first is the cancellation token. I don’t want the display task to be dependent on the cancellation token, so I’m passing CancellationToken.None. But I want this task to run only if the compute task returns some result. For this purpose I need to choose one of the TaskContinuationOptions. In my case the best solution is simply not to run the display task if the compute task was canceled. I can use the NotOnCanceled option that does just that.

But tasks may fail for some other reason, not just because of cancellation. I don’t want to display results of any failed task, so I’m choosing the OnlyOnRanToCompletion option. I’ll explain the “ran to completion” concept more a little bit later in this post.

However, the previous version with the try-catch block could also inform me that the tasks were indeed canceled. (Each task either printed the result or reported a cancellation by printing the exception message.) How to do the same in this version? Well, I can create a new task that will run only if the task is canceled. I’m going to convert the display task into two different tasks:

var displayResults = compute.ContinueWith(resultTask =>
                     textBlock1.Text += "root " + j.ToString() + " " +
                                            compute.Result.ToString() +
                                            Environment.NewLine,
                         CancellationToken.None,
                         TaskContinuationOptions.OnlyOnRanToCompletion,
                         ui);

var displayCancelledTasks = compute.ContinueWith(resultTask =>
                               textBlock1.Text += "root " + j.ToString() +
                                                  " canceled" +
                                                  Environment.NewLine,
                               CancellationToken.None,
                               TaskContinuationOptions.OnlyOnCanceled, ui);

Now I get the same results as I did with the try-catch block, and I don’t have to deal with exceptions at all. Let me emphasize that this is a more natural way of working with TPL and tasks, and I’d recommend that you to use this approach whenever possible.

By the way, if you click the Cancel button and then click the Start button in my application, all you will see is a list of canceled tasks and no results at all. The cancellation token got into the canceled state, and there is no way to turn it back to “not canceled.” You have to create a new token each time. But in my case it’s easy. I simply add the following line at the beginning of the event handler for the Start button:

tokenSource = new CancellationTokenSource();

Now let’s take a look at the output of my little program. (I clicked Cancel right after I saw the results of the 5th root.)

tasks

You can see that after I canceled the operation some roots were calculated nonetheless. This is a good illustration of the task cancellation concept in .NET. After I called the Cancel method for the task cancellation token, tasks that were already running switched into the “run to completion” mode, so I got the results even after I canceled the tasks. The tasks that were still waiting in the queue were indeed canceled.

What if I don’t want to waste resources and want to stop all computations immediately after I click Cancel? In this case, I have to periodically check for the status of the cancellation token somewhere within the method that performs the long-running operation. Since I declared the cancellation token as a field, I can simply use it within the method.

public double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
    {
        tokenSource.Token.ThrowIfCancellationRequested();
        result += Math.Exp(Math.Log(i) / root);
    }
    return result;
}

I made two changes: I removed the static keyword from the method declaration to enable field access, and I added a line that checks for the status of the cancellation token. The ThrowIfCancellationRequested method indicates so-called “cooperative cancellation,” which means that the task throws an exception to show that it accepted the cancellation request and will stop working.

In this case, the thrown exception is handled by the TPL, which transitions the task to the canceled state. You cannot and should not handle this exception in your code. However, Visual Studio checks for all unhandled exceptions and shows them when in debug mode. So, if you now press F5, you’re going to see this exception. Basically, you need to ignore it: You can simply press F5 several times to continue or run the program by using Ctrl+F5 to avoid debug mode.

Another possibility is to switch off the checking of these “unhandled by the user code” exceptions in Visual Studio: Go to Tools -> Options -> Debugging -> General, and clear the Just My Code check box. This makes Visual Studio “swallow” this exception. However, this may cause side effects in your debugging routine, so check the MSDN documentation to make sure it’s the right option for you.

Well, that’s all what I wanted to show you this time. Here is the final code of my still surprisingly small program.

public partial class MainWindow : Window
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    public MainWindow()
    {
        InitializeComponent();
    }
    public double SumRootN(int root)
    {
        double result = 0;
        for (int i = 1; i < 10000000; i++)
        {
            tokenSource.Token.ThrowIfCancellationRequested();
            result += Math.Exp(Math.Log(i) / root);
        }
        return result;
    }

    private void start_Click(object sender, RoutedEventArgs e)
    {
        tokenSource = new CancellationTokenSource();
        
        textBlock1.Text = "";
        label1.Content = "Milliseconds: ";

        var watch = Stopwatch.StartNew();
        List<Task> tasks = new List<Task>();
        var ui = TaskScheduler.FromCurrentSynchronizationContext();
        for (int i = 2; i < 20; i++)
        {
            int j = i;
            var compute = Task.Factory.StartNew(() =>
            {
                return SumRootN(j);
            }, tokenSource.Token);

            tasks.Add(compute);

            var displayResults = compute.ContinueWith(resultTask =>
                                 textBlock1.Text += "root " + j.ToString() + " " +
                                                        compute.Result.ToString() +
                                                        Environment.NewLine,
                                     CancellationToken.None,
                                     TaskContinuationOptions.OnlyOnRanToCompletion,
                                     ui);

            var displayCancelledTasks = compute.ContinueWith(resultTask =>
                                           textBlock1.Text += "root " + j.ToString() +
                                                              " canceled" +
                                                              Environment.NewLine,
                                           CancellationToken.None,
                                           TaskContinuationOptions.OnlyOnCanceled, ui);
        }

        Task.Factory.ContinueWhenAll(tasks.ToArray(),
            result =>
            {
                var time = watch.ElapsedMilliseconds;
                label1.Content += time.ToString();
            }, CancellationToken.None, TaskContinuationOptions.None, ui);
    }

    private void cancel_Click(object sender, RoutedEventArgs e)
    {
        tokenSource.Cancel();
        textBlock1.Text += "Cancel" + Environment.NewLine;
    }

}

As usual, here are some links for further reading if you want to know more about task cancellation:

 

P.S.

Thanks to Dmitry Lomov, Michael Blome, and Danny Shih for reviewing this and providing helpful comments, to Mick Alberts for editing.