CancellationTokenSource not behaving as expected

2020-03-23 17:41发布

问题:

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();
    }
}

回答1:

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);


回答2:

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.