5

what's expected in this case, is that if the user cancels the task by hitting enter, the other task hooked by ContinueWith will run, but it's not the case, as per an AggregateException keeps thrown despite the explicit handling in the ContinueWith which is apparently not being executed.
any clarification on the below please?

class Program
{
    static void Main(string[] args)
    {
        CancellationTokenSource tokensource = new CancellationTokenSource();
        CancellationToken token = tokensource.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.WriteLine("Press any key to cancel");
        Console.ReadLine();
        tokensource.Cancel();
        task.Wait();
    }
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
Tawfik Khalifeh
  • 939
  • 6
  • 21

2 Answers2

8

Let's start with a few facts:

  1. When you pass a CancellationToken as a parameter for Task.Run it only has an effect if it's cancelled before the task started running. If the task is already running it will not be canceled.
  2. To get a task canceled after it has started running, you need to use CancellationToken.ThrowIfCancellationRequested, not CancellationToken.IsCancellationRequested.
  3. If a task is canceled, its Exception property doesn't hold any exceptions and is null.
  4. If a continuation task does not run for some reason, that means it was canceled.
  5. A task contains exceptions from itself + all its child tasks (hence, AggregateException).

So this is what happens in your code:

The task starts running, because the token is not canceled. It will run until the token gets canceled. After it will end the continuation will not run because it only runs when the preceding task is canceled, and it hasn't been. When you Wait the task it will throw an AggregateException with a TaskCanceledException because the continuation was canceled (if you would remove that continuation the exception will go away).

Solution:

You need to fix the task so it would actually be canceled, and remove (or null check) the exception handling because there is no exception:

var task = Task.Run(new Action(() =>
{
    while (true)
    {
        token.ThrowIfCancellationRequested();
        Console.Write("*");
        Thread.Sleep(1000);
    }
}), token).ContinueWith(
    t => Console.WriteLine("You have canceled the task"),
    TaskContinuationOptions.OnlyOnCanceled);
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • According to [Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.throwifcancellationrequested?view=netframework-4.8) and also to [.NET Framework source code](https://github.com/microsoft/referencesource/blob/master/mscorlib/system/threading/CancellationToken.cs), `ThrowIfCancellationRequested` uses `IsCancellationRequested` property, so there should be no difference by using either one, regarding to get a task canceled before or after the task is running. – Alexandre Dec 10 '19 at 06:48
  • @Alexandre the difference is that `ThrowIfCancellationRequested` will cancel the task and `IsCancellationRequested` will not. – i3arnon Dec 11 '19 at 07:23
  • Ok, of course, `IsCancellationRequested` is only a `bool` property and peeking at its value won't throw any exception. But the answer states that _when you pass a CancellationToken […] it only has an effect if it's cancelled before the task started running. If the task is already running it will not be canceled. To get a task canceled after it has started running, you need to use ThrowIfCancellationRequested, not IsCancellationRequested._ Regarding to that, both ways will behave the same: if the CancellationToken **is** canceled, both will identify it. – Alexandre Dec 13 '19 at 17:35
  • 1
    @Alexandre you can use `IsCancellationRequested` to be notified of cancellation and to have your code complete the task. But while the task would end, it will not be **cancelled**. And the continuation with the `TaskContinuationOptions.OnlyOnCanceled` option will not run. – i3arnon Dec 14 '19 at 15:22
4

If you pass the token as the second parameter, the task won't continue on nicely because it's really been cancelled. Instead it throws an OperationCanceledException which gets wrapped in an AggregateException. This is entirely expected. Now, if you did NOT pass the token to the task constructor then you'd see the behaviour you'd expect because because you'd only be using the token as a flag for exiting the while loop. In that case you're not really cancelling the task, you're exiting the while loop and completing the task normally.

x0n
  • 51,312
  • 7
  • 89
  • 111