10

I am trying to cancel a Task<> by calling the CancellationTokenSource.Cancel() method within the task, but I cannot get it to work.

Here is the code I am using:

TaskScheduler ts = TaskScheduler.Current;

CancellationTokenSource cts = new CancellationTokenSource();

Task t = new Task( () =>
{
    Console.WriteLine( "In Task" );
    cts.Cancel();
}, cts.Token );

Task c1 = t.ContinueWith( antecedent =>
{
    Console.WriteLine( "In ContinueWith 1" );
}, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, ts );

Task c2 = c1.ContinueWith( antecedent =>
{
    Console.WriteLine( "In ContinueWith 2" );
}, TaskContinuationOptions.NotOnCanceled );

t.Start();

Console.ReadKey();

Environment.Exit( 1 );

This print outs:

In Task
In ContinueWith 1
In ContinueWith 2

What I expected was this:

In Task

Am I missing something here? Can tasks only be cancelled outside of the task?

Intrepid
  • 2,781
  • 2
  • 29
  • 54
  • I think, You don't understand principle of cancellation. See `CancellationToken.ThrowIfCancellationRequested` method. – Hamlet Hakobyan Nov 12 '12 at 13:19
  • That's because I have only starting learning TPL. – Intrepid Nov 12 '12 at 13:28
  • I think this is a really good question, was surprised to be able to reproduce. – Johan Larsson Nov 12 '12 at 13:29
  • @JaroslawWaliszko I could not get it to be non-deterministic, put it in a loop and ran it 10000 times with same result. – Johan Larsson Nov 12 '12 at 13:40
  • @Johan Larsson: the loop version is on the other hand always predictable, as You've said. I was talking about running couple of times by using `ctrl+f5` in VS, which for some reason gives me various outpus. Mabye it's the case of flushing the result to console only. – jwaliszko Nov 12 '12 at 13:51
  • I see (and expected) deterministic results for this case. – Matt Smith Nov 12 '12 at 15:00

2 Answers2

10

A task is only considered "cancelled" if

  • Its cancellation token is cancelled before it starts executing.
  • Its cancellation token is cancelled while it is executing and the code cooperatively observes the cancellation by throwing the OperationCancelledException (usually by the code calling cts.Token.ThrowIfCancellationRequested())

If you added a cts.Token.ThrowIfCancellationRequested() line after the cts.Cancel() then things would behave as you expect. In your example, the cancellation takes place while the task is running, but the task doesn't observe the cancellation, and the task's action runs to completion. So the task is marked as "Ran to Completion".

You can check for the case of a task that "Ran to Completion" but was cancelled (either during or after the completion of the task) by checking the cancellation token within the continuation (cts.Token.IsCancellationRequested). Another option that sometimes helps is to use the original cancellation token as the cancellation token for the continuation (that way, if the the cancellation is not noticed by the antecedent task, it will at least be respected by the continuation)--since the TPL will mark the continuation as cancelled before it even has a chance to run.

Matt Smith
  • 17,026
  • 7
  • 53
  • 103
  • I will accept your answer as it makes sense. I have also managed to get it to work as expected using the info you supplied. – Intrepid Nov 12 '12 at 15:39
  • @MattSmith: what do you mean by `original cancellation token as the cancellation token for the continuation`.. Would you please mind to provide code snippet for it by editing the answer.. – techBeginner Nov 16 '12 at 04:51
  • I mean passing in the same cancellation token to the `new Task` call *and* the `ContinueWith` call rather than passing in `CancellationToken.None`. Does that help? – Matt Smith Nov 16 '12 at 05:52
  • 2
    I just want to emphasize the criteria for "canceled" here, because I found the official MSDN documentation misleading. It implies that a task can cancel itself by throwing an `OperationCanceledException`. But this ONLY works when it's in response to a cancellation request. Even if you pass in the cancellation token, this will still be treated as a regular exception unless you have previously called `CancellationTokenSource.Cancel()` -- the task will go to `IsFaulted` state, not `IsCanceled`. – nmclean Jun 20 '13 at 14:07
  • @nmclean, that's not quite right. The requirement for causing the Task to go into cancelled state is that the task's action throw an `OperationCanceledException` (OCE) (using the constructor that takes a `CancellationToken` (CT)) and that the `OCE`'s `CT` matches the `Task`s `CT`. The `Task`'s `CT` is considered to be the one you passed in to `Task.Factory.StartNew(...)` or `Task.Run` (i.e just passing it in to the lambda doesn't allow the TPL to know it should be associated with the Task. Note: this is different than async/await, where any `OCE` causes the Task to be considered cancelled. – Matt Smith Jun 20 '13 at 18:44
  • 2
    @MattSmith That's exactly what I thought when I read the documentation, but it isn't what I'm seeing. Have you tested it? Here's my test: http://pastebin.com/U7LcNvk0 The output I get is `Faulted`. If I uncomment `cts.Cancel`, I get `Canceled`. – nmclean Jun 20 '13 at 20:26
  • @nmclean, No I haven't it (I'll try out your code tomorrow), but I believe you after looking at your code (originally, I assumed you weren't passing in the `CT` to the `StartNew(...)`. So, they must be doing a check of whether the `CancellationToken` is actually Cancelled or not. Interesting--thanks for pointing this out. – Matt Smith Jun 20 '13 at 20:48
  • @nmclean, I verified I get the same behavior--so that's really interesting that you can't self cancel unless you gave the Task access to the `CancellationTokenSource`. I guess it gives more control to the caller--they know with certainty that the only way a Task can go into a cancelled state is if they cancelled it. Async/Await doesn't give you this control, since any OperationCanceledException will cause the Task to go into the Cancelled state. See: http://stackoverflow.com/questions/15257356/associate-a-cancellationtoken-with-an-async-methods-taks – Matt Smith Jun 21 '13 at 03:09
0

This snippet works as you expected.

var cts = new CancellationTokenSource();

            var t = new Task(() =>
            {
                Console.WriteLine("In Task");
                cts.Cancel();
            }, cts.Token);

            Task c1 = t.ContinueWith(antecedent =>
            {
                Console.WriteLine("In ContinueWith 1");
            }, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled/* by definition from MSDN here must be NotOnCanceled*/, TaskScheduler.Current);

            c1.ContinueWith(antecedent =>
            {
                Console.WriteLine("In ContinueWith 2");
            }, TaskContinuationOptions.NotOnCanceled);

            t.Start();
            Console.ReadKey();

            Environment.Exit(1);

But seems there are the bug in TaskContinuationOptions enum.

NotOnCanceled Specifies that the continuation task should not be scheduled if its antecedent was canceled. This option is not valid for multi-task continuations.

OnlyOnCanceled Specifies that the continuation task should be scheduled only if its antecedent was canceled. This option is not valid for multi-task continuations.

Hamlet Hakobyan
  • 32,965
  • 6
  • 52
  • 68
  • If you comment out the `cts.Cancel();` line it doesn't execute the second ContinueWith() as I expected. I was expecting `In ContinueWith 2` to be displayed. – Intrepid Nov 12 '12 at 14:07