Parallel Programming: Task Cancellation

Parallel Programming: Task Cancellation

Rate This
  • Comments 26

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.

Leave a Comment
  • Please add 2 and 4 and type the answer here:
  • Post
  • The TokenSource and associated Token are fully thread-safe.  You never pass the actual TokenSource to any other thread so the other threads simply call the thread-safe Token::ThrowIfCancellationRequested method directly.

    Except I just noticed this IS using the TokenSource, something like below should be used instead:

    public double SumRootN(int root, Token token)

    {

       double result = 0;

       for (int i = 1; i < 10000000; i++)

       {

           token.ThrowIfCancellationRequested();

           result += Math.Exp(Math.Log(i) / root);

       }

       return result;

    }

    .

    .

    .

    var compute = Task.Factory.StartNew(() =>

    {

       return SumRootN(j, tokenSource.Token);

    }, tokenSource.Token);

  • Before I can ask my colleagues to use this otherwise excellent article as a point of reference, the code is in need of some cleanup.

       CancellationTokenSource tokenSource = new CancellationTokenSource();

    // ....

           tokenSource = new CancellationTokenSource();

    Any reason why tokenSource is initialized in the declaration? That looks like a leftover from the previous version of this code. It is cleaner to test if tokenSource==null in cancel_Click() instead of making sure the object is always created.

    Ordinarily I am all for "left as an exercise for the reader", but this is MS', what, third or fourth attempt at trying to lure regular developers to utilize some sort of threading library. IMO this requires absolute 100% top-notch quality code in all sample code and documentation.

  • @Nemo. I found myself adding the Token param also as I went through the code. Definite improvement

    @@codesbad. I agree the initialization should not be done in the declaration by the final code iteration, but hey - it doesn't sour an otherwise *very* useful offering for this target audience, IMO. In fact I can't remember the last time I saw some meaty material from MS that was this well produced.

    @Alexandra. Great job, will look for more of the same from you! Cheers

  • I understand the usefulness of throwing to cancel immediately out of a thread, but is using exceptions to control flow of a program considered best practice? It seems to me that if you are writing cooperateive cancellation, you should gracefully end the thread without throwing.

    If you do use the ThrowIfCancellationRequested() method, and need to use it in a try block, how can you avoid catching it? is this acceptable?

    try

    {

           tokenSource.Token.ThrowIfCancellationRequested();

           result += Math.Exp(Math.Log(i) / root);

    }

    catch (System.OperationCanceledException)

    {

           throw;

    }

    catch (Exception e)

    {

           // Handle e

    }

  • Add a Thread.Sleep into the iteration code with a duration of say 5 seconds. Then 1 second after the loop has began, close the form. The threads can't exit even though you diligently made such a request in the form_closing event as each thread is locked in the ''pretend long running 3rd party web-service call or library call' that (Thread.Sleep is supposed to represent this). The user thinks the app has closed, but it ain't. =/

    Further, if the loop logic raises events and those events are subscribed to by the UI, in which the UI perhaps increments a progress-bar and event fired when the form is closed and long gone, the event would execute code that raises exceptions on deallocated form resources.

    Said user is now baffled as an error and an application debug/shut down request dialogue appears from no where.... I think we need a thread abort construct adding to this new library, and the dev should handle resource deallocation etc within the Thread.Abort Exception catch they put in the iteration logic.

    I hope MS don't expect me to implement my own TaskShedualer to do this.. I think they do.. oh dear time to get the sledge hammer out. =(

  • @Alexandra Rusina

    >> When i want to make a task to be long-running,

    >> I have to pass the creation option to be long running,

    >> but then the Taskscheduler is required,

    >> then how do i pass the taskscheduler parameter?

    >

    > You can simply pass null, it should be OK.

    Actually MSDN specifies that an ArgumentNullException "is thrown when the scheduler argument is null" and my tests confirm this.

    You may want to use TaskScheduler.Current instead.

  • Thank you.

    You freed me from all that multithreading, background work , UI locking, Passing Data to a Thread Horror !!!

    Thank you.

  • thx

  • Great job Alexandra. Thanks alot for the excellent article as well as series.

    But I have a Question.

    Is it possible to cancel an individual task by not affecting other tasks? like we do in traditional threading(something like threadName.Abort( ) ).

    Waiting for some reply.

  • I tried this piece of code , but it doesnt cancel :( i dont know why is happening that :/ . I am using net framework 4.5 :) Can anyone help me? PLEASE!

  • Would you please this post for .Net 4.5.1 and async/await.

    Thank you.

Page 2 of 2 (26 items) 12