Sending a Cancellation Token in a Task<TResult> - c#

I have an async method that I want to be able to cancel that is currently called
string html = await Html.WebClientRetryAsync(state);
I have been trying to figure out the syntax to be able to call this method passing it a CancellationToken. Here is what I have attempted so far.
CancellationToken ct;
Func<object, Task<string>> func = async (s) => await WebClientRetryAsync((string)s);
Task<Task<string>> task = Task<Task<string>>.Factory.StartNew(func, state.Uri.AbsoluteUri, ct);
string html = await task.Result;
I plan to check inside the method to see if cancellation was requested prior to proceeding. I couldn't find any examples in the documentation
What I have won't get the token to the WebClientRetryAsync method, so this won't work.

If the underlying async method simply doesn't support cancellation in any way then there simply isn't any way for you to cancel it, and that's that, as is discussed in How do I cancel non-cancelable async operations?. You can have your code continue executing on after the cancellation token is cancelled, but if the underlying asynchronous operation has no way of being told it should stop, you have no way of stopping it.
If you do just want a way of moving on when the task finishes, assuming the timeout is reached, then you can use these extension methods to add that functionality:
public static Task AddCancellation(this Task task, CancellationToken token)
{
return Task.WhenAny(task, task.ContinueWith(t => { }, token))
.Unwrap();
}
public static Task<T> AddCancellation<T>(this Task<T> task,
CancellationToken token)
{
return Task.WhenAny(task, task.ContinueWith(t => t.Result, token))
.Unwrap();
}
Realize of course that if you await the result of a call like this and the task is cancelled, it will throw an exception, which is how you would support that path.

Related

TPL task continuation: task is in state Faulted instead of Canceled when cancelled

When cancelling the following task, the task is not in state Canceled but Faulted:
private string ReturnString()
{
// throw new OperationCanceledException(_cancellationToken); // This puts task in faulted, not canceled
Task.Delay(5000, _cancellationToken).Wait(_cancellationToken); // Simulate work (with IO-bound call)
// throw new OperationCanceledException(_cancellationToken); // This puts task in faulted, not canceled
// _cancellationToken.ThrowIfCancellationRequested(); // This puts task in faulted, not canceled
// throw new Exception("Throwing this exception works!"); // This works as expected (faulted)
return "Ready";
}
private void SetReturnValueWithTaskContinuation()
{
SynchronizationContext synchronizationContext = SynchronizationContext.Current;
Task<string> task = Task.Run(() => ReturnString());
task.ContinueWith(
antecedent =>
{
if (antecedent.Status == TaskStatus.Canceled)
{
synchronizationContext.Post(result => _txtResultContinueWith.Text = (string)result, "Cancelled");
}
else if (antecedent.Status == TaskStatus.Faulted)
{
synchronizationContext.Post(result => _txtResultContinueWith.Text = (string)result, "Exception");
}
else
{
synchronizationContext.Post(result => _txtResultContinueWith.Text = (string)result, antecedent.Result);
}
});
}
I know, that the cancellation token has to be supplied when throwing an OperationCanceled Exception. I know, there are two ways of throwing an OperationCanceled Exception where the ThrowIfCancellationRequested() is the prefered one. And I know, that the cancellation token of the continuation chain should be different than the cancellation token of the task to cancel, otherwise the continuation chain will be canceled too. For the sake of simplification, I only use one cancellation token to cancel the task itself.
But, the task has state "Faulted" and not "Canceled". Is that a bug? If not, than it is a usability issue of the TPL. Can somebody help?
Task.Run does want us to provide a cancellation token for proper propagation of the cancellation status, see:
Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested
This is particularly important if we use the overrides of Task.Run that accept Action, or Func<T> delegate where T is anything but Task. Without a token, the returned task status will be Faulted rather than Canceled in this case.
However, if the delegate type is Func<Task> or Func<Task<T>> (e.g., an async lambda), it gets some special treatment by Task.Run. The task returned by the delegate gets unwrapped and its cancellation status is properly propagated. So, if we amend your ReturnString like below, you'll get the Canceled status as expected, and you don't have to pass a token to Task.Run:
private Task<string> ReturnString()
{
Task.Delay(5000, _cancellationToken).Wait(_cancellationToken);
return Task.FromResult("Ready");
}
// ...
Task<string> task = Task.Run(() => ReturnString()); // Canceled status gets propagated
If curious about why Task.Run works that way, you can dive into its implementation details.
Note though, while this behavior has been consistent from when Task.Run was introduced in .NET 4.5 through to the current version of .NET Core 3.0, it's still undocumented and implementation-specific, so we shouldn't rely upon it. For example, using Task.Factory.StartNew instead would still produce Faulted:
Task<string> task = Task.Factory.StartNew(() => ReturnString(),
CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current).Unwrap();
Unless your associate a cancellation token, the only time when Task.Factory.StartNew would return a Canceled task here is when ReturnString has been modified to be async. Somehow, the compiler-generated async state machine plumbing changes the behavior of the Task returned by ReturnString.
In general, it's always best to provide a cancellation token to Task.Run or Task.Factory.StartNew. In your case, if this is not possible, you may want to make ReturnString an async method:
private async Task<string> ReturnString()
{
var task = Task.Run(() =>
{
Thread.Sleep(1500); // CPU-bound work
_cancellationToken.ThrowIfCancellationRequested();
});
await task; // Faulted status for this task
// but the task returned to the caller of ReturnString
// will be of Canceled status,
// thanks to the compiler-generated async plumbing magic
return "Ready";
}
Usually, doing async-over-sync is not a good idea, but where ReturnString is a private method implementing some GUI logic to offload work to a pool thread, this is might the way to go.
Now, you might only ever need to wrap this with another Task.Run if you wanted to take it off the current synchronization context (and if even you do so, the cancellation status will still be correctly propagated):
Task<string> task = Task.Run(() => ReturnString());
On a side note, a common pattern to not worry about synchronization context is to routinely use ConfigureAwait everywhere:
await Task.Run(...).ConfigureAwait(continueOnCapturedContext: false);
But I myself have stopped using ConfigureAwait unconsciously, for the following reason:
Revisiting Task.ConfigureAwait(continueOnCapturedContext: false)

What is the use of Cancellation token here

I've the following chunk of code :
using (var cancelSource = new CancellationTokenSource())
{
Task[] tasks = null;
var cancelToken = cancelSource.Token;
tasks = new[]
{
Task.Run(async () => await ThrowAfterAsync("C", cancelToken, 1000)) //<---
};
await Task.Delay(howLongSecs * 1000); // <---
cancelSource.Cancel();
await Task.WhenAll(tasks);
}
Where ThrowAfterAsync have this :
private async Task ThrowAfterAsync(string taskId, CancellationToken cancelToken, int afterMs)
{
await Task.Delay(afterMs, cancelToken);
var msg = $"{taskId} throwing after {afterMs}ms";
Console.WriteLine(msg);
throw new ApplicationException(msg);
}
Resharper is suggesting me that I can use the overload of Task.Run() with cancellation token like this :
Task.Run(async () => await ThrowAfterAsync("C", cancelToken, 1000), cancelToken)
But why ? What is the benefit of doing this over the first version, without the cancellation token as parameter ?
In this specific case, there is no point. In general, you'd want to do as it suggests because, by passing the token to Task.Run it can avoid even scheduling the operation in the first place if the token is cancelled before the operation even has a chance to start, but in your case you're creating the token and you know it won't be cancelled when you start the operation.
But the reason you don't need to pass the token to Task.Run is because the code starting that task is the operation responsible for cancelling the token, and so it knows that the token isn't cancelled yet. Normally you'd be accepting a token from elsewhere, and you wouldn't know if/when it was cancelled.
All that said, there's no reason to even use Task.Run at all. You can just write:
tasks = new[] { ThrowAfterAsync("C", cancelToken, 1000) };
It will have the same behavior but without needlessly starting a new thread just to start the asynchronous operation.
Next, your code will never return in less than howLongSecs seconds, even if the operation finishes before then, because of how you've structured your code. You should simply provide the timeout to the cancellation token source and let it take care of canceling the token at the right time, it won't delay the rest of your method if the operation finishes before the cancellation should happen, so your whole method can just be written as:
using (var cancelSource = new CancellationTokenSource(Timespan.FromSeconds(howLongSecs)))
{
await ThrowAfterAsync("C", cancelToken, 1000)
}
Resharper sees that you are using a method (Task.Run) which has overload which accepts CancellationToken, you have instance of CancellationToken in scope, but you do not use that overload which accepts a token. It does not perform any extensive analysys of your code - it's as simple as that. You can easily verify this with this code:
class Program {
static void Main() {
CancellationToken ct;
Test("msg"); // will suggest to pass CancellationToken here
}
private static void Test(string msg) {
}
private static void Test(string msg, CancellationToken ct) {
}
}
Yes the code itself is strange and you don't need to wrap your async in Task.Run at all, but I won't touch that since you asked just why Resharper suggests that.

How to safely cancel a task using a CancellationToken and await Task.WhenAll

I have a framework which creates a CancellationTokenSource, configures CancelAfter, then calls an async method and passes the Token. The async method then spawns many tasks, passing the cancellation token to each of them, and then awaits the collection of tasks. These tasks each contain logic to gracefully cancel by polling IsCancellationRequested.
My issue is that if I pass the CancellationToken into Task.Run() an AggregateException is thrown containing a TaskCanceledException. This prevents the tasks from gracefully canceling.
To get around this I can not pass the CancelationToken into Task.Run, however I'm not sure what I will be losing. For instance, I like the idea that if my task hangs and cannot perform the graceful cancel this exception will force it down. I was thinking I could string along two CancelationTokens to handle this, one 'graceful' and the other 'force'. However, I don't like that solution.
Here is some psudo-code representing what I described above..
public async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(30000);
await this.Run(cts.Token);
}
public async Task Run(CancellationToken cancelationToken)
{
HashSet<Task> tasks = new HashSet<Task>();
foreach (var work in this.GetWorkNotPictured)
{
// Here is where I could pass the Token,
// however If I do I cannot cancel gracefully
// My dilemma here is by not passing I lose the ability to force
// down the thread (via exception) if
// it's hung for whatever reason
tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken))
}
await Task.WhenAll(tasks);
// Clean up regardless of if we canceled
this.CleanUpAfterWork();
// It is now safe to throw as we have gracefully canceled
cancelationToken.ThrowIfCancellationRequested();
}
public static void DoWork(work, cancelationToken)
{
while (work.IsWorking)
{
if (cancelationToken.IsCancellationRequested)
return // cancel gracefully
work.DoNextWork();
}
}
I recommend that you follow the standard cancellation pattern of throwing an exception rather than just returning:
public static void DoWork(work, cancellationToken)
{
while (work.IsWorking)
{
cancellationToken.ThrowIfCancellationRequested();
work.DoNextWork();
}
}
If you have cleanup work to do, that's what finally is for (or using, if you can refactor that way):
public async Task Run(CancellationToken cancellationToken)
{
HashSet<Task> tasks = new HashSet<Task>();
foreach (var work in this.GetWorkNotPictured)
{
tasks.Add(Task.Run(() => this.DoWork(work, cancellationToken))
}
try
{
await Task.WhenAll(tasks);
}
finally
{
this.CleanUpAfterWork();
}
}
Provide the CancellationToken to Task.Run in addition to passing it to the method doing the work. When you do this Task.Run can see that the exception thrown was caused by the CancellationToken it was given, and will mark the Task as cancelled.
tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken),
cancelationToken));
Once you've done this you can ensure that DoWork throws when the token is cancelled, rather than checking IsCancellationRequested to try to end by being marked as "completed successfully".

How to transform task.Wait(CancellationToken) to an await statement?

So, task.Wait() can be transformed to await task. The semantics are different, of course, but this is roughly how I would go about transforming a blocking code with Waits to an asynchronous code with awaits.
My question is how to transform task.Wait(CancellationToken) to the respective await statement?
await is used for asynchronous methods/delegates, which either accept a CancellationToken and so you should pass one when you call it (i.e. await Task.Delay(1000, cancellationToken)), or they don't and they can't really be canceled (e.g. waiting for an I/O result).
What you can do however, is abandon* these kinds of tasks with this extension method:
public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
return task.IsCompleted // fast-path optimization
? task
: task.ContinueWith(
completedTask => completedTask.GetAwaiter().GetResult(),
cancellationToken,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
Usage:
await task.WithCancellation(cancellationToken);
* The abandoned task doesn't get cancelled, but your code behaves as though it has. It either ends with a result/exception or it will stay alive forever.
To create a new Task that represents an existing task but with an additional cancellation token is quite straightforward. You only need to call ContinueWith on the task, use the new token, and propagate the result/exceptions in the body of the continuation.
public static Task WithCancellation(this Task task,
CancellationToken token)
{
return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task,
CancellationToken token)
{
return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
This allows you to write task.WithCancellation(cancellationToken) to add a token to a task, which you can then await.
From https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#cancelling-uncancellable-operations
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
// This disposes the registration as soon as one of the tasks trigger
using (cancellationToken.Register(state =>
{
((TaskCompletionSource<object>)state).TrySetResult(null);
},
tcs))
{
var resultTask = await Task.WhenAny(task, tcs.Task);
if (resultTask == tcs.Task)
{
// Operation cancelled
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
}
Here is another solution:
Task task;
CancellationToken token;
await Task.WhenAny(task, Task.Delay(Timeout.Infinite, token));
See this answer that talks about using Task.Delay() to create a Task from a CancellationToken. Here are the docs for Task.WhenAny and Task.Delay.

How to create a cancellable task loop?

Is it possible to use System.Threading.Task.Task to create a loop of task that can be cancelled?
The flow should start with a Task.Delay(x ms) then continue with userdefined task, then another Task.Delay(y ms) and repeat from the user defined task.
var result = Task.Delay(initialDelay)
.ContinueWith(t => dostuff..)
.ContinueWith what goes here?
Is it even doable using tasks?
I could spin up a timer and be done with it, but using task seems to be the right way to go if I need cancellation, no?
await makes this super easy:
public async Task TimedLoop(Action action,
CancellationToken token, TimeSpan delay)
{
while (true)
{
token.ThrowIfCancellationRequested();
action();
await Task.Delay(delay, token);
}
}
Without async (but still just using the TPL) it's a bit messier. I generally solve this problem by having a continuation that attaches itself to a variable of type Task. This works fine, but it can take a second to wrap your head around it. Without await it may be easier to just use a Timer instead.
public Task TimedLoop(Action action,
CancellationToken token, TimeSpan delay)
{
//You can omit these two lines if you want the method to be void.
var tcs = new TaskCompletionSource<bool>();
token.Register(() => tcs.SetCanceled());
Task previous = Task.FromResult(true);
Action<Task> continuation = null;
continuation = t =>
{
previous = previous.ContinueWith(t2 => action(), token)
.ContinueWith(t2 => Task.Delay(delay, token), token)
.Unwrap()
.ContinueWith(t2 => previous.ContinueWith(continuation, token));
};
previous.ContinueWith(continuation, token);
return tcs.Task;
}

Categories

Resources