61

OK, my questions is really simple. Why this code does not throw TaskCancelledException?

static void Main()
{
    var v = Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 10;
    }, new CancellationTokenSource(500).Token).Result;

    Console.WriteLine(v); // this outputs 10 - instead of throwing error.
    Console.Read();
}

But this one works

static void Main()
{
    var v = Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 10;
    }, new CancellationToken(true).Token).Result;

    Console.WriteLine(v); // this one throws
    Console.Read();
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
Aliostad
  • 80,612
  • 21
  • 160
  • 208

4 Answers4

55

Cancellation in Managed Threads:

Cancellation is cooperative and is not forced on the listener. The listener determines how to gracefully terminate in response to a cancellation request.

You didn't write any code inside your Task.Run method to access your CancellationToken and to implement cancellation - so you effectively ignored the request for cancellation and ran to completion.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • 1
    please see updated question - If it is cooperative, why second one works? – Aliostad Mar 25 '14 at 14:43
  • The CancellationTokenSource is setup with a timeout. If Task.Delay is used instead of Thread.Sleep you can get the exception to throw. – Darrel Miller Mar 25 '14 at 14:43
  • 12
    @Aliostad - `Tasks` may sit waiting to be scheduled for any amount of time before their code actually runs. An obvious optimization, for a task that has been provided with a cancellation token, is to check that token before you actually schedule the task on a thread. In your second case, when that check is performed, cancellation has already been requested so the task is never actually started. – Damien_The_Unbeliever Mar 25 '14 at 14:46
46

There is a difference in canceling a running task, and a task scheduled to run.

After the call to the Task.Run method, the task is only scheduled, and probably have not been executed yet.

When you use the Task.Run(..., CancellationToken) family of overloads with cancellation support, the cancellation token is checked when the task is about to run. If the cancellation token has IsCancellationRequested set to true at this time, an exception of the type TaskCanceledException is thrown.

If the task is already running, it is the task's responsibility to call the ThrowIfCancellationRequested method, or just throw the OperationCanceledException.

According to MSDN, it's just a convenience method for the following:

if (token.IsCancellationRequested) throw new OperationCanceledException(token);

Note the different kind of exception used in this two cases:

catch (TaskCanceledException ex)
{
    // Task was canceled before running.
}
catch (OperationCanceledException ex)
{
    // Task was canceled while running.
}

Also note that TaskCanceledException derives from OperationCanceledException, so you can just have one catch clause for the OperationCanceledException type:

catch (OperationCanceledException ex)
{
    if (ex is TaskCanceledException)
        // Task was canceled before running.
    // Task was canceled while running.
}
George Polevoy
  • 7,450
  • 3
  • 36
  • 61
  • 1
    *"...the cancellation token is checked when the task is about to run."* -- In case the execution of the `Task.Run` `action` is delayed because the `ThreadPool` is saturated, the cancellation of the token has immediate effect because it is callback-based. The `Task.Run` infrastructure doesn't just check the `IsCancellationRequested` property a couple of times. It registers a callback through the [`CancellationToken.Register`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.register) mechanism. – Theodor Zoulias Feb 25 '23 at 12:04
27

In the first variant of your code, you're not doing anything to manage the cancellation token.

For example, you're not checking whether token.IsCancellationRequested returns true (and then throwing an exception) or calling ThrowIfCancellationRequested() from your CancellationToken object.

Also, the Task.Run overload you used checks whether the token is already canceled or not when the task is about to start and your code states that token will report cancellation after 500 ms. So, your code is just ignoring the cancellation request and that's why the task ran to completion.

You should do something like this:

void Main()
{
    var ct = new CancellationTokenSource(500).Token;
     var v = 
     Task.Run(() =>
    {
        Thread.Sleep(1000);
        ct.ThrowIfCancellationRequested();
        return 10;
    }, ct).Result;
    
    Console.WriteLine(v); //now a TaskCanceledException is thrown.
    Console.Read();
}

or this, without passing the token, as others already noted:

void Main()
{
    var ct = new CancellationTokenSource(500).Token;
    ct.ThrowIfCancellationRequested();
    var v = 
     Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 10;
    }).Result;
    
    Console.WriteLine(v); //now a TaskCanceledException is thrown.
    Console.Read();
}

The second variant of your code works, because you're already initializing a token with a Canceled state set to true. Indeed, as stated here:

If canceled is true, both CanBeCanceled and IsCancellationRequested will be true

the cancellation has already been requested and then the exception TaskCanceledException will be immediately thrown, without actually starting the task.

Alberto Solano
  • 7,972
  • 3
  • 38
  • 61
  • 8
    But what's the point of passing ct to Task.Run? – Zhiliang Dec 12 '17 at 03:35
  • @Zhiliang Are you familiar with TPL? The "ct" variable is passed to Task.Run to be able to cancel the task. In this case, the above user asked why his code didn't throw any exception using a CancellationToken that would be cancelled after 500 ms. The reason is that the code didn't do anything to deal with cancellation. – Alberto Solano Dec 12 '17 at 10:28
  • 7
    Since `ct.ThrowIfCancellationRequested();` actually references the `ct` variable in the closure, I can get the same result if I ommit the `ct` that is passed to `Task.Run()`. Right? – Zhiliang Jan 02 '18 at 06:27
  • @Zhiliang Yes, you can get the same result (exception thrown), although this means your task will not have cancellation support. The code snippet you see in my answer was simply copied and pasted from the question, and accordingly modified just to show what asked in the question, not caring too much about 100% "perfect" code. – Alberto Solano Jan 03 '18 at 08:58
  • 4
    Also have the question: What's the difference for Task.Run(task, cancel) vs Task.Run(task)? The ct variable is passed to Task.Run, but does not seem to be in use anywhere. – Karata Mar 06 '18 at 05:44
  • @Karata The difference is explained in the [documentation about Task.Run and its overloads](https://msdn.microsoft.com/en-us/library/hh195051(v=vs.110).aspx). Passing ct (or CancellationToken) to Task.Run, we're saying that we want to cancel the task returned from Task.Run. – Alberto Solano Mar 09 '18 at 09:26
  • 2
    Passing ct will auto cancel the task before it started. If not, you should check the ct status in the code, and the task is running. – Jim Apr 11 '21 at 05:17
9

Another implementation using Task.Delay with token instead it Thread.Sleep.

 static void Main(string[] args)
    {
        var task = GetValueWithTimeout(1000);
        Console.WriteLine(task.Result);
        Console.ReadLine();
    }

    static async Task<int> GetValueWithTimeout(int milliseconds)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        cts.CancelAfter(milliseconds);
        token.ThrowIfCancellationRequested();

        var workerTask = Task.Run(async () =>
        {
            await Task.Delay(3500, token);
            return 10;
        }, token);

        try
        {
            return await workerTask;
        }
        catch (OperationCanceledException )
        {
            return 0;
        }
    }
Z.R.T.
  • 1,543
  • 12
  • 15