I am testing a simple console application created on Linqpad, idea is to have assured understanding of the working of Task and create a logic which works, when Task is completed, faulted or Canceled. I want to execute a logic only when Task is completed but not faulted or canceled.
void Main()
{
CancellationTokenSource cts = new CancellationTokenSource(new TimeSpan(0,0,0,0,1000));
Task t = Task.Run(() => Work(),cts.Token);
try
{
t.Wait();
}
catch
{
}
("Completed :: " + t.IsCompleted).Dump();
("Canceled :: " + t.IsCanceled).Dump();
("Faulted :: " + t.IsFaulted).Dump();
}
public async Task Work()
{
await Task.Delay(3000);
}
Following are the issues:
I am able to confidently figure out the Completed and Faulted states, but even when in my view this code should lead to Task cancellation, the value of IsCanceled property is always false.
Ideally when the Task is faulted, even though I am silently capturing the exception in a try catch block, it should show IsCompleted as false, but it always remain true, currently Linqpad doesn't have continue on error option, but I am assuming, it would turn false if I can continue on error
I am able to confidently figure out the Completed and Faulted states, but even when in my view this code should lead to Task cancellation, the value of IsCanceled property is always false.
There is no automatism in cancellation. You are passing CancellationToken to Task.Run. If cancellation would occur while the task is starting, the start process would be interrupted by cancellation. Once the task is running, it is the task's method's responsibility to check the cancellation token. Wait is not doing that. It does not even know of the cancellation token. Hence, the task can never turn into the canceled state.
This is how you would observe cancellation:
void Main()
{
CancellationTokenSource cts = new CancellationTokenSource(new TimeSpan(0,0,0,0,1000));
Task t = Task.Run(() => Work(cts.Token),cts.Token);
try
{
t.Wait();
}
catch
{
}
("Completed :: " + t.IsCompleted).Dump();
("Canceled :: " + t.IsCanceled).Dump();
("Faulted :: " + t.IsFaulted).Dump();
}
public async Task Work(CancellationToken token)
{
await Task.Delay(3000, token);
}
Ideally when the Task is faulted, even though I am silently capturing the exception in a try catch block, it should show IsCompleted as false, but it always remain true
Check MSDN:
IsCompleted will return true when the task is in one of the three final states: RanToCompletion, Faulted, or Canceled.
Others have noted that your code is not observing the CancellationToken, and that's why the task is not being cancelled.
I'll answer this part of the question:
I want to execute a logic only when Task is completed but not faulted or canceled.
To do this, put your logic after you await the task:
await t;
// Your logic here.
Using IsCanceled / IsFaulted / IsCompleted for control flow is a code smell.
You did not pass the CancellationToken to the Task.Delay method, so nothing had to be cancelled.
The token you pass in Task.Run(xxx) prevents the work from ever being started if the token has an outstanding cancellation. But your token is cancelled after 1 second, that is long after the call to Task.Run.
Try this:
void Main()
{
CancellationTokenSource cts = new CancellationTokenSource(new TimeSpan(0, 0, 0, 0, 1000));
Task t = Task.Run(() => Work(cts.Token), cts.Token);
try
{
t.Wait();
}
catch
{
}
("Completed :: " + t.IsCompleted).Dump();
("Canceled :: " + t.IsCanceled).Dump();
("Faulted :: " + t.IsFaulted).Dump();
}
public async Task Work(CancellationToken t)
{
await Task.Delay(3000, t);
}
Related
This question already has an answer here:
Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested
(1 answer)
Closed last month.
According to this and this, passing a cancellation token to a task constructor, or Task.Run, will cause the task to be associated with said token, causing the task to transition to Canceled instead of Faulted if a cancellation exception occurs.
I've been fiddling with these examples for a while, and I can't see any benefits other than preventing a cancelled task to start.
Changing the code on this MSDN example from
tc = Task.Run(() => DoSomeWork(i, token), token);
to
tc = Task.Run(() => DoSomeWork(i, token));
produced the exact same output:
This code also results in two cancelled state tasks with the same exceptions thrown:
var token = cts.Token;
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
var t2 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
}, token);
Console.ReadKey();
try
{
cts.Cancel();
Task.WaitAll(t1, t2);
}
catch(Exception e)
{
if (e is AggregateException)
{
foreach (var ex in (e as AggregateException).InnerExceptions)
{
Console.WriteLine(e.Message);
}
}
else
Console.WriteLine(e.Message);
}
Console.WriteLine($"without token: { t1.Status }");
Console.WriteLine($"with token: { t2.Status }");
Console.WriteLine("Done.");
Apparently, throwing OperationCanceledException from within the task is enough to make it transition to Canceled instead of Faulted. So my question is: is there a reason for passing the token to the task other than preventing a cancelled task to run?
Is there a reason for passing the token to the task other than preventing a cancelled task to run?
In this particular case, No. Past the point in time that the task has started running, the token has no effect to the outcome.
The Task.Run method has many overloads. This case is peculiar because of the infinite while loop.
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
The compiler has to choose between these two overloads:
public static Task Run(Action action);
public static Task Run(Func<Task> function);
...and for a reason explained in this question it chooses the later. Here is the implementation of this overload:
public static Task Run(Func<Task?> function, CancellationToken cancellationToken)
{
if (function == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.function);
// Short-circuit if we are given a pre-canceled token
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
// Kick off initial Task, which will call the user-supplied function and yield a Task.
Task<Task?> task1 = Task<Task?>.Factory.StartNew(function, cancellationToken,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
// Create a promise-style Task to be used as a proxy for the operation
// Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown
// as faults from task1, to support in-delegate cancellation.
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1,
lookForOce: true);
return promise;
}
The important detail is the lookForOce: true. Let's look inside the UnwrapPromise class:
// "Should we check for OperationCanceledExceptions on the outer task and interpret them
// as proxy cancellation?"
// Unwrap() sets this to false, Run() sets it to true.
private readonly bool _lookForOce;
..and at another point below:
case TaskStatus.Faulted:
List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo oceEdi;
if (lookForOce && edis.Count > 0 &&
(oceEdi = edis[0]) != null &&
oceEdi.SourceException is OperationCanceledException oce)
{
result = TrySetCanceled(oce.CancellationToken, oceEdi);
}
else
{
result = TrySetException(edis);
}
break;
So although the internally created Task<Task?> task1 ends up in a Faulted state, its unwrapped version ends up as Canceled, because the type of the exception is
OperationCanceledException (abbreviated as oce in the code).
That's a quite convoluted journey in the history of TPL, with methods introduced at different times and frameworks, in order to serve different purposes. The end result is a little bit of inconsistency, or nuanced behavior if you prefer to say it so. A relevant article that you might find interesting is this: Task.Run vs Task.Factory.StartNew by Stephen Toub.
I have a Windows Service that monitors my application by running a couple of tests every second. A bug report has been submitted that said that the service stoppes running after a while, and I'm trying to figure out why.
I suspect that the code below is the culprit, but I have trouble understanding exactly how it works. The ContinueWith statement has recently been commented out, but I dont know if it is needed
private Task CreateTask(Action action)
{
var ct = _cts.Token;
return Task.Run(async () =>
{
ct.ThrowIfCancellationRequested();
var sw = new Stopwatch();
while (true)
{
sw.Restart();
action();
if (ct.IsCancellationRequested)
{
_logger.Debug("Cancellation requested");
break;
}
var wait = _settings.loopStepFrequency - sw.ElapsedMilliseconds;
if (wait <= 0) // No need to delay
continue;
// If ContinueWith is needed wrap this in an ugly try/catch
// handling the exception
await Task.Delay(
(int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds),
ct); //.ContinueWith(tsk => { }, ct);
}
_logger.Debug("Task was cancelled");
}, _cts.Token);
}
Are there any obvious problems with this code?
Are there any obvious problems with this code?
The one that jumps out to me is the calculation for the number of milliseconds to delay. Specifically, there's no floor. If action() takes an unusually long time, then the task could fail in a possibly unexpected way.
There are several ways for the task to complete in either a cancelled or failed state, or it can delay forever:
The task can be cancelled before the delegate begins, due to the cancellation token passed to Task.Run.
The task can be cancelled by the ThrowIfCancellationRequested call.
The task can complete successfully after being cancelled, due to the IsCancellationRequested logic.
The task can be cancelled by the cancellation token passed to Task.Delay.
The task may fail with an ArgumentOutOfRangeException if _settings.loopStepFrequency - sw.ElapsedMilliseconds is less than -1. This is probably a bug.
The task may delay indefinitely (until cancelled) if _settings.loopStepFrequency - sw.ElapsedMilliseconds happens to be exactly -1. This is probably a bug.
To fix this code, I recommend two things:
The code is probably intending to do await Task.Delay((int) wait, ct); instead of await Task.Delay((int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds), ct);. This will remove the last two conditions above.
Choose one method of cancellation. The standard pattern to express cancellation is via OperationCanceledExcpetion; this is the pattern used by ThrowIfCancellationRequested and by Task.Delay. The IsCancellationRequested check is using a different pattern; it will successfully complete the task on cancellation, instead of cancelling it.
There are so many problems with this code, that makes more sense to rewrite it than attempt to fix it. Here is a possible way to rewrite this method, with some (possibly superfluous) argument validation added:
private Task CreateTask(Action action)
{
if (action == null) throw new ArgumentNullException(nameof(action));
var ct = _cts.Token;
var delayMsec = _settings.loopStepFrequency;
if (delayMsec <= 0) throw new ArgumentOutOfRangeException("loopStepFrequency");
return Task.Run(async () =>
{
while (true)
{
var delayTask = Task.Delay(delayMsec, ct);
action();
await delayTask;
}
}, ct);
}
The responsibility for logging a possible exception/cancellation belongs now to the caller of the method, that (hopefully) awaits the created task.
var task = CreateTask(TheAction);
try
{
await task; // If the caller is async
//task.GetAwaiter().GetResult(); // If the caller is sync
_logger.Info("The task completed successfully");
}
catch (OperationCanceledException)
{
_logger.Info("The task was canceled");
}
catch (Exception ex)
{
_logger.Error("The task failed", ex);
}
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)
I've got the following example:
public void Run()
{
var ctc = new CancellationTokenSource();
try
{
DoAsync(ctc).Wait();
Console.WriteLine("Done");
}
catch (AggregateException exception)
{
Console.WriteLine("Inside try-catch block");
Console.WriteLine();
Console.WriteLine(exception);
exception.Handle(ex =>
{
Console.WriteLine(ex.Message);
return true;
});
}
}
private async Task DoAsync(CancellationTokenSource ctc)
{
Console.WriteLine("DoAsync started");
await Task.Run(() =>
Console.WriteLine("DoAsync Run"),
ctc.Token
)
.ContinueWith(antecedent =>
Console.WriteLine("DoAsync Run cancelled"),
TaskContinuationOptions.OnlyOnCanceled
);
Console.WriteLine("DoAsync finished");
}
I've created a method (DoAsync) that does some asynchronous work and can be cancelled at any time.
As you can see Task.Run gets a cancellation token. For this reason I created continuation task with continuationOptions = TaskContinuationOptions.OnlyOnCanceled.
As a result I expected continuation task to be called only when cancellation is requested and in other cases - ignored.
But in my implementation task returned by ContinueWith throws an exception when its antecedent task is not being cancelled:
DoAsync started
DoAsync Run
Inside try-catch block
System.AggregateException...
A task was canceled.
I can fix this by adding another ContinueWith as in the example below:
await Task.Run(() =>
Console.WriteLine("DoAsync Run"),
ctc.Token
)
.ContinueWith(antecedent =>
Console.WriteLine("DoAsync Run cancelled"),
TaskContinuationOptions.OnlyOnCanceled
)
.ContinueWith(antecedent => { });
And this code doesn't throw any exceptions.
But can I handle the cancellation using single ContinueWith properly?
The remarks for ContinueWith specifically state:
If the continuation criteria specified through the continuationOptions parameter are not met, the continuation task will be canceled instead of scheduled.
Since the criteria you specified for the antecedent weren't met, (namely, it wasn't cancelled) the continuation was set to be cancelled. You awaited the cancelled task, which therefore result in DoAsync faulting with an operation cancelled exception.
I'm trying to test a scenario where I have a task that can be cancelled, and a continuation that should be running if the antecedent task doesn't complete. A sample of the code is like this:
static void Main(string[] args)
{
var source = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
{
while (true)
{
source.Token.ThrowIfCancellationRequested();
}
}, source.Token);
var continuation = task.ContinueWith(t =>
{
Console.WriteLine("Continuation");
if (t.Status == TaskStatus.Faulted)
{
Console.WriteLine("Antecedent Faulted: " + t.Exception.Message);
}
}, source.Token, TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.AttachedToParent, TaskScheduler.Current);
var cancellation = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
source.Cancel();
});
try
{
continuation.Wait();
}
catch (AggregateException)
{
Console.WriteLine("AggregateException");
}
Console.WriteLine("Done");
while (!Console.KeyAvailable) { }
}
The output of this program is:
AggregateException
Done
To a certain extent, I get it. The primary task was cancelled, which ends up throwing a TaskCanceledException which gets wrapped in the AggregateException. The question is, is this expected behaviour? If so, what use is TaskStatus.Faulted in a continuation, if that continuation isn't getting executed? I even set a looping check for ConsoleKeyAvailable just in case the continuation does get run as well as the AggregateException getting thrown.
I believe the reason this is occurring is you are passing source.Token as the CancellationToken for the call to task.ContinueWith. Despite passing NotOnRanToCompletion as the continuation options, the fact that the token is cancelled before the continuation task ever starts means the scheduler can immediately transition it to the canceled state without actually running it.