In the reference book for Microsoft's 70-483 exam about using CancellationToken, the first way to cancel with signal is through throwing an exception, and then it introduces the second:
Instead of catching the exception, you can also add a continuation Task that executes only when the Task is canceled. In this Task, you have access to the exception that was thrown, and you can choose to handle it if that’s appropriate. Listing 1-44 shows what such a continuation task would look like
Here is the List 1-44:
Task task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{
t.Exception.Handle((e) => true);
Console.WriteLine("You have canceled the task");
}, TaskContinuationOptions.OnlyOnCanceled);
And this is my full Main method code:
static void Main(string[] args)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
Task task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{
t.Exception.Handle((e) => true);
Console.WriteLine("You have canceled the task");
}, TaskContinuationOptions.OnlyOnCanceled);
Console.ReadLine();
cancellationTokenSource.Cancel();
task.Wait();
Console.ReadLine();
}
However, unlike what it is stated, when I press Enter, the exception (AggregationException) still thrown to the Main method at task.Wait()
call. Moreover, if I remove that call, the second Task never runs (no exception is thrown). Is there anything I do wrong? Is it possible to handle the exception without using try-catch
?
To explicitly state the problem, your second continuation is not executing, but you think it should:
static void Main(string[] args)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
Task task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{ // THIS
t.Exception.Handle((e) => true); // ISN'T
Console.WriteLine("You have canceled the task"); // EXECUTING
}, TaskContinuationOptions.OnlyOnCanceled);
Console.ReadLine();
cancellationTokenSource.Cancel();
task.Wait();
Console.ReadLine();
}
The second continuation is not executing because you must use token.ThrowIfCancellationRequested()
in order to trigger it:
Task task = Task.Run(() =>
{
while (true)
{
token.ThrowIfCancellationRequested(); // <-- NOTICE
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{
Console.WriteLine("From Continuation: " + t.Status);
Console.WriteLine("You have canceled the task");
}, TaskContinuationOptions.OnlyOnCanceled);
// OUTPUT:
// ***
// From Continuation: Canceled
// You have canceled the task
The second continuation was called because the task.Status
was Canceled
. This next snippet does not trigger the second continuation, because the task.Status
is not set to Canceled
:
Task task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{
Console.WriteLine("From Continuation: " + t.Status);
Console.WriteLine("You have canceled the task");
}, TaskContinuationOptions.OnlyOnCanceled);
// OUTPUT:
// AggregationException
As stated, the second continuation was not called. Let's force its execution by removing the OnlyOnCanceled
clause:
Task task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{
Console.WriteLine("From Continuation: " + t.Status);
Console.WriteLine("You have NOT canceled the task");
}); // <-- OnlyOnCanceled is gone!
// OUTPUT:
// ***
// From Continuation: RanToCompletion
// You have NOT canceled the task
// (no AggregationException thrown)
Notice that even though .Cancel()
was called, the task.Status
from within the continuation is RanToCompletion
. Also notice no AggregationException
is thrown. This shows that just calling .Cancel()
from the token source doesn't set the task status to Canceled
.
When only .Cancel()
is called and .ThrowIfCancellationRequested()
is not called, the AggregationException
is actually an indicator of successful task cancellation. To quote the MSDN article:
If you are waiting on a
Task
that transitions to theCanceled
state, aSystem.Threading.Tasks.TaskCanceledException
exception (wrapped in anAggregateException
exception) is thrown. Note that this exception indicates successful cancellation instead of a faulty situation. Therefore, the task'sException
property returnsnull
.
Which leads me to the grand conclusion:
Your t.Exception...
line has been omitted from all of my code, because "the task's Exception
property returns null
" upon successful cancellation. The line should have been omitted from Listing 1-44. It looks like they were conflating the following two techniques:
OnlyOnCanceled
continuation is called and no exception is thrown.OnlyOnCanceled
continuation is NOT called and an AggregationException is thrown for you to handle at Task.Wait()
Disclaimer: both snippets are valid ways to cancel a task, but they probably have differences in behavior that I'm not aware of.
A task instance that is canceled with cancellationTokenSource.Cancel()
will have the TaskStatus.RanToCompletion
state, not to the TaskStatus.Canceled
state. So I think that you have to change the TaskContinuationOptions.OnlyOnCanceled
to TaskContinuationOptions.OnlyOnRanToCompletion
You can check Task Cancellation on MSDN for more details.
Here is a the sample code:
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
Task task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.Write("*");
Thread.Sleep(1000);
}
}, token).ContinueWith((t) =>
{
t.Exception.Handle((e) => true);
Console.WriteLine("You have canceled the task");
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Console.ReadLine();
cancellationTokenSource.Cancel();
try
{
task.Wait();
}
catch (AggregateException e)
{
foreach (var v in e.InnerExceptions)
Console.WriteLine(e.Message + " " + v.Message);
}
Console.ReadLine();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With