Reputation: 17474
Question: Is there a way to associate a CancellationToken
with the Task
returned from an async
method?
Generally, a Task
will end up in the Canceled
state if an OperationCancelledException
is thrown with a CancellationToken
matching the Task
's CancellationToken
. If they don't match, then the task goes into the Faulted
state:
void WrongCancellationTokenCausesFault()
{
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
cts2.Cancel();
// This task will end up in the Faulted state due to the task's CancellationToken
// not matching the thrown OperationCanceledException's token.
var task = Task.Run(() => cts2.Token.ThrowIfCancellationRequested(), cts1.Token);
}
With async
/await
, I haven't found a way to set the method's Task
's CancellationToken
(and thus achieve the same sort of functionality). From my testing, it seems that any OperationCancelledException
will cause the async
method to enter the Cancelled state:
async Task AsyncMethodWithCancellation(CancellationToken ct)
{
// If ct is cancelled, this will cause the returned Task to be in the Cancelled state
ct.ThrowIfCancellationRequested();
await Task.Delay(1);
// This will cause the returned Task to be in the Cancelled state
var newCts = new CancellationTokenSource();
newCts.Cancel();
newCts.Token.ThrowIfCancellationRequested();
}
It would be nice to have a little more control, since if a method I call from my async
method is cancelled (and I don't expect cancellation--i.e. its not this Task
's CancellationToken
), I would expect the task to enter the Faulted
state--not the Canceled
state.
Upvotes: 4
Views: 4280
Reputation: 43951
Here is a Run
method that attempts to imitate the behavior of the Task.Run
method, when supplied with a CancellationToken
argument. The task returned by the Run
can only become Canceled
if the async method returns a Canceled
task, and also the associated CancellationToken
matches the supplied argument.
/// <summary>
/// Invokes an async method (a method implemented with the async keyword), and
/// returns a proxy of the produced async task. In case the async task completes
/// in a Canceled state but the causative CancellationToken is not equal with the
/// cancellationToken argument, the proxy transitions to a Faulted state.
/// In all other cases, the proxy propagates the status of the async task as is.
/// </summary>
public static Task<TResult> Run<TResult>(Func<Task<TResult>> asyncMethod,
CancellationToken cancellationToken)
{
return asyncMethod().ContinueWith(t =>
{
if (t.IsCanceled)
{
TaskCanceledException tce = new(t);
// In case the async method has been canceled by an unknown token,
// propagate a faulted task.
if (tce.CancellationToken != cancellationToken)
return Task.FromException<TResult>(tce);
}
return t; // In any other case, propagate the task as is.
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
.Unwrap();
}
Usage example:
Task<int> task = Run(async () =>
{
await Task.Delay(1000, new CancellationToken(true));
return 13;
}, new CancellationToken(false));
try { task.Wait(); } catch { }
Console.WriteLine(task.Status);
Output:
Faulted
Upvotes: 1
Reputation: 457342
I think the design works well for the common case: if any child operations are cancelled, then the cancellation propagates to the parent (the most common case is that the parent and child share cancellation tokens).
If you want different semantics, you can catch
the OperationCanceledException
in your async
method and throw an exception that fits the semantics you need. If you want to use these semantics repeatedly, an extension method for Task
should fit the bill.
Upvotes: 2