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:
- Getting Started
- Task Schedulers and Synchronization Context
- Task Cancellation (this post)
- Blocking Collection and the Producer-Consumer Problem
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.)
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:
- Documentation about the new .NET 4 cancellation model in general and task cancellation in particular on MSDN.
- The .NET 4 Cancellation Framework and Cancellation in Parallel Extensions from the Parallel Programming with .NET blog.
- The Debugger does not correctly handle Task exceptions? from the Parallel Programming with .NET blog.
P.S.
Thanks to Dmitry Lomov, Michael Blome, and Danny Shih for reviewing this and providing helpful comments, to Mick Alberts for editing.
Comments
Anonymous
July 19, 2010
This is an excellent article (and series), great job Alexandra!Anonymous
July 20, 2010
This series is great. There's a whole load of information out there, but it's nice to have a walkthrough of some of the key concepts. Keep up the good work :)Anonymous
July 21, 2010
When you cancel a task behaind the scene Thread,abort accoured and thred abort exception is swallen by the Clr?Anonymous
July 22, 2010
Great post. Thanks a lot. keep coming.Anonymous
July 23, 2010
The comment has been removedAnonymous
July 23, 2010
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?Anonymous
July 27, 2010
@benny You can simply pass null, it should be OK. I guess you are talking about TaskCreationOptions.LongRunning. It's not strictly required to use this option for long-running operations, it just provides more information to the compiler and may provide additional benefits. But you can simply create a task without specifying this option.Anonymous
July 31, 2010
Thanks for the answer. looking forward to more posts.Anonymous
August 02, 2010
Series is excellent.Anonymous
August 05, 2010
This was very good series of blogs. Though, I am sure that I missed something, but I have one question... I thought the the compute Task was running on a different thread to the UI's thread. If this is true, then how can the MainWindow.SumRootN() method safely access the MainWindow.tokenSource private field member in order to call the tokenSource.Token.ThrowIfCancellationRequested() method?Anonymous
August 06, 2010
The comment has been removedAnonymous
August 11, 2010
Excellent series. I hope you will continue on this topic (and others ;-). I really like your step-by-step approach!Anonymous
August 16, 2010
I think this article is bad for the following reasons.
- Accessing field 'tokenSource' from the UI thread and task threads without synchronization. -> tokenSource = new CancellationTokenSource(); -> tokenSource.Token.ThrowIfCancellationRequested(); If you want to do that, u either must be sure that all tasks were completed and ensure that only one thread accesses it or put some synchronization mechanism to access field tokenSource. This is not so bad in this case, but people see these sort of examples and think that everything is trivial and soon start sharing objects that are not thread safe (i know that CancellationTokenSource is thead safe, but other objects may not be, and IN EVERY CASE accessing field IS NOT THEAD SAFE).
Task.Factory.ContinueWhenAll(tasks.ToArray(), result => { var time = watch.ElapsedMilliseconds; label1.Content += time.ToString(); }, CancellationToken.None, TaskContinuationOptions.None, ui); Updating the UI from background thread is also bad. Maybe i missed something, and understood something wrong. Can somebody share a comment.
Anonymous
August 17, 2010
The comment has been removedAnonymous
August 17, 2010
Ok, i was wrong in the second case. My fault. Sorry. But i still think that ACCESSING the field is not thread safe.Anonymous
November 17, 2010
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);Anonymous
November 24, 2010
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.Anonymous
December 08, 2010
@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! CheersAnonymous
December 08, 2010
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 }Anonymous
January 14, 2011
The comment has been removedAnonymous
November 15, 2011
@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.Anonymous
November 25, 2011
Thank you. You freed me from all that multithreading, background work , UI locking, Passing Data to a Thread Horror !!! Thank you.Anonymous
May 02, 2013
thxAnonymous
September 15, 2013
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.Anonymous
November 30, 2013
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!Anonymous
March 09, 2014
Would you please this post for .Net 4.5.1 and async/await. Thank you.