The documentation for Task.IsCanceled specifies that OperationCanceledException.CancellationToken has to match the cancellation token used to start the task in order to properly acknowledge. I'm confused about how this works within an asynchronous continuation. There appears to be some undocumented behavior that allows them to acknowledge despite not knowing their cancellation token ahead of time.
static void TestCancelSync()
{
// Create a CTS for an already running task.
using var cts = new CancellationTokenSource();
cts.Cancel();
// Attempt to acknowledge cancellation of the CT (doesn't work).
cts.Token.ThrowIfCancellationRequested();
}
static async Task TestCancelAsync()
{
await Task.Yield();
// Create a CTS for an already running continuation.
using var cts = new CancellationTokenSource();
cts.Cancel();
// Acknowledge cancellation of the CT (how is this possible?).
cts.Token.ThrowIfCancellationRequested();
}
var syncCancelTask = Task.Run(TestCancelSync);
var asyncCancelTask = TestCancelAsync();
try
{
await Task.WhenAll(syncCancelTask, asyncCancelTask);
}
catch (OperationCanceledException)
{
// Prints "{ syncCanceled = False, asyncCanceled = True }"
Console.WriteLine(
new
{
syncCanceled = syncCancelTask.IsCanceled,
asyncCanceled = asyncCancelTask.IsCanceled,
});
}
I'd like to understand how this is working under the hood. When exactly is it necessary to acknowledge cancellation for a specific token? Can I trust that any tasks from an async method will have this undocumented behavior?
Indeed. The documentation of the
Task.IsCanceledprobably hasn't been updated since its introduction with the .NET Framework 4.0 (2010). It looks like it is associated with the behavior of theTask.Factory.StartNewmethod, which takes a (sometimes misunderstood)CancellationTokenas argument, and takes into account this argument when theactiondelegate fails with anOperationCanceledException. Asynchronous methods that are implemented with theasynckeyword do not work this way. They always complete in theCanceledstate whenever anOperationCanceledExceptionis thrown, regardless of the identity of theCancellationTokenthat caused the cancellation. In your question you are experimenting with anasyncmethod, so you get this behavior, which deviates from the documentation of theTask.IsCanceledproperty.If for some reason you want the
Task.Factory.StartNewbehavior with yourasyncmethods, see this question for ideas.