Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task.ContinueWith does not work with OnlyOnCanceled

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?

like image 236
Luke Vo Avatar asked Oct 15 '15 19:10

Luke Vo


2 Answers

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 the Canceled state, a System.Threading.Tasks.TaskCanceledException exception (wrapped in an AggregateException exception) is thrown. Note that this exception indicates successful cancellation instead of a faulty situation. Therefore, the task's Exception property returns null.

Which leads me to the grand conclusion:

Listing 1-44 is a known error.

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:

  1. The first snippet of my answer is a valid way to cancel a task. The OnlyOnCanceled continuation is called and no exception is thrown.
  2. The second snippet of my answer is also a valid way to cancel a task, but the 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.

like image 106
kdbanman Avatar answered Nov 19 '22 00:11

kdbanman


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();
like image 31
S.Dav Avatar answered Nov 19 '22 00:11

S.Dav