2
votes

Reading Task-based Asynchronous Pattern by Stephen Toub I'm trying to see how cancellation works for tasks. In the section Consuming the Task-based Asynchronous Pattern under Await, in 3-rd paragraph it says:

If the Task or Task TResult> awaited ended in the Canceled state, an OperationCanceledException will be thrown.

I'm trying to see this in action in the code below.

static void Main(string[] args)
{
   CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
   CancellationToken cancellationToken = cancellationTokenSource.Token;

   Task<int> valueTask = DoStuffAsync(cancellationToken);
   Thread.Sleep(TimeSpan.FromSeconds(1));
   cancellationTokenSource.Cancel();

   Console.WriteLine("value task's status: {0}", valueTask.Status); 
   Console.ReadLine();
}

And the DoStuffAsync() method

static async Task<int> DoStuffAsync(CancellationToken cancellationToken)
{
    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
    return 42;
}

Executing this code doesn't throw any exception, it just prints:

value task's status: Cancelled

Now, my expectation was in DoStuffAsync() method since await Task.Delay(...) is cancelled, we have awaited task ended in Canceled state, hence exception should have been thrown (according to quote from the TAP document), but if I put a breakpoint on Console.ReadLine() and check valueTask it's Status is Cancelled, and Exception is null.

Can anybody help me understand if I misread the document, or code I come up with is not properly reproducing the case?

3
The quote talks about what happens when you await a cancelled task. You never await the task! Nobody said Task.Exception would have an exception, that's just silly :) You have to understand that you have two separate threads of execution here, and you never synchronize them - where exactly would you expect the exception to happen? On the thread pool, killing your entire application? - Luaan

3 Answers

3
votes

The method returns the task itself and the result itself is never accessed. If you would try to access valueTask.Result, you would get a TaskCanceledException (inside an AggregateException).

Likewise, if you would await valueTask (Main would have to by async), you would try to obtain the result in which case the exception would also be thrown. This is the behaviour described in the mentioned paragraph.

The clue is that the Task object is valid, but the result is not because the code after await is never executed if the task is cancelled. For example with:

static async Task<int> DoStuffAsync(CancellationToken cancellationToken)
{
    var delay = Task.Delay(5000, cancellationToken);
    Console.WriteLine("before await"); 
    await delay;
    Console.WriteLine("after await"); 
    return 42;
}

The second writeline is never executed if the task is cancelled. As long as no result is accessed of the task returned by DoStuffAsync, the task is a valid object, just cancelled. Accessing the result will force the runtime to acknowledge that the task was never finished and throw an exception.

If the async method does not return a task, you would also get a TaskCancelled Exception:

static async void Main()
{
   CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
   CancellationToken cancellationToken = cancellationTokenSource.Token;

   DoStuffAsync(cancellationToken);
   Thread.Sleep(TimeSpan.FromSeconds(1));
   cancellationTokenSource.Cancel();
   Console.ReadLine();
}

// Define other methods and classes here
static async void DoStuffAsync(CancellationToken cancellationToken)
{
    await Task.Delay(5000, cancellationToken);
}

Because there is no task object to be returned which can contain the status, the compiler has to warn the executing code that it could not completely run the asynchronous code and an exception is thrown.

2
votes

You will get OperationCanceledException when you will try to get result from this task.

Try to add this line in the end of your Main method:

valueTask.GetAwaiter().GetResult();

and you will get TaskCanceledException that is desendent from OperationCanceledException, await of canceled task also will throw TaskCanceledException.

You can also use valueTask.Result and valueTask.Wait() but they will throw AggregateException with single member TaskCanceledException.

For better understanding of what's happening inside await it may be useful to see how it's transformed into state machine at tryroslyn.

0
votes

Edit: This answer is for the case where you want to use the TaskCanceledException inside of your Task, but your question is about getting it outside of it but it may still be useful to some. ;-)

In your Task (DoAsyncStuff function) you have to check the CancellationToken and then throw the TaskCanceledException by yourself or just call ThrowIfCancellationRequested() on it.