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?
The documentation for
Task.IsCanceled
specifies thatOperationCanceledException.CancellationToken
has to match the cancellation token used to start the task in order to properly acknowledge.
Indeed. The documentation of the Task.IsCanceled
probably hasn't been updated since its introduction with the .NET Framework 4.0 (2010). It looks like it is associated with the behavior of the Task.Factory.StartNew
method, which takes a (sometimes misunderstood) CancellationToken
as argument, and takes into account this argument when the action
delegate fails with an OperationCanceledException
. Asynchronous methods that are implemented with the async
keyword do not work this way. They always complete in the Canceled
state whenever an OperationCanceledException
is thrown, regardless of the identity of the CancellationToken
that caused the cancellation. In your question you are experimenting with an async
method, so you get this behavior, which deviates from the documentation of the Task.IsCanceled
property.
If for some reason you want the Task.Factory.StartNew
behavior with your async
methods, see this question for ideas.