How to handle tasks that are canceled without triggering a cancellation itself? - c#

I'm trying to get tasks in C# to work for a specific use case but I'm not understanding how task continuation options affect the flow of tasks.
What I'm trying to do is get a series of tasks chained together with ContinueWith. This will look something like this:
A -> B -> C -> D
However, I want to include the option to short-circuit this in the event an error, so it should look like this:
A -> B -> C -> D -> X
So I put "OnlyOnRanToCompletion" as the task continuation option for each of the ContinueWith functions. Then, to catch the cancellation and return an error, I put a final task at the end of the chain with the task continuation option set to "OnlyOnCanceled".
The problem is that when this last block is hit, the continuation option is not met and the task then gets set to cancelled even if the original series of tasks was never cancelled.
What I want to happen is have A through D run, and if one of them results in a cancellation, skip the rest and run X. If A through D complete, the task shouldn't cancel. The solution needs to support an arbitrary number of continuations and will be created using LINQ.Expressions, so using async/await is probably not going to fly unless it's done creatively.
Some sample code that exhibits this is:
var cts = new CancellationTokenSource();
var token = cts.Token;
var t = Task.FromResult(1)
.ContinueWith(
x => x.Result + 1,
token,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default)
.ContinueWith(
x => x.Result + 1,
token,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default)
.ContinueWith(
x => -1,
token,
TaskContinuationOptions.OnlyOnCanceled,
TaskScheduler.Default);
The expected behavior here would be to return 3, and the status not completed.
The actual result is that the task is cancelled.
How do I do this?
Also, I can't use async because my goal is to piggyback off TPL inside of something compiled from LINQ.Expressions so that it can evaluate asynchronously and handle errors at the end without throwing any exceptions.

Figured it out - to get the last continuation to run regardless of whether or not the previous continuations completed and without setting the status to canceled do this:
Change the continuation option of the last continuation to TaskContinuation.None so that it always runs, so it won't cancel if it gets here with a status of completed.
Don't pass in a cancellation token to the last continuation because passing in a cancellation token that has been cancelled seems to have the effect of causing the continuation to cancel if it would have otherwise run without the token.

See the remarks for ContinueWith for an explanation of this behavior:
The returned Task will not be scheduled for execution until the current task has completed. If the criteria specified through the continuationOptions parameter are not met, the continuation task will be canceled instead of scheduled.
Since the criteria for your last ContinueWith call weren't met, the Task returned from that call was cancelled.

Related

Has my collegue reproduced the exact behavior of Task.WhenAll<TResult>?

When reviewing work by a contractor, I came upon the following extension method:
public static class TaskExtensions
{
public static async Task<IEnumerable<TResult>> WhenAllSynchronousExecution<TResult>(this IEnumerable<Task<TResult>> tasks, CancellationToken token = default(CancellationToken))
{
var results = new List<TResult>();
var exceptions = new List<Exception>();
foreach (var task in tasks)
{
if (task == null)
{
throw new ArgumentNullException(nameof(tasks));
}
if (token.IsCancellationRequested) {
exceptions.Add(new OperationCanceledException("Tasks collection cancelled", token));
break;
}
try
{
results.Add(await task);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any()) {
throw new AggregateException(exceptions);
}
return results;
}
}
To this looks to be exactly what Task.WhenAll<TResult> already does. quote the remarks:
The call to WhenAll(IEnumerable<Task>) method does not block the calling thread. However, a call to the returned Result property does block the calling thread.
If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state, where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.
If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.
If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state. The Task.Result property of the returned task will be set to an array containing all of the results of the supplied tasks in the same order as they were provided (e.g. if the input tasks array contained t1, t2, t3, the output task's Task.Result property will return an TResult[] where arr[0] == t1.Result, arr[1] == t2.Result, and arr[2] == t3.Result).
If the tasks argument contains no tasks, the returned task will immediately transition to a RanToCompletion state before it's returned to the caller. The returned TResult[] will be an array of 0 elements.
So to me it seems they've recreated an existing method. The only difference seems to be the checking of the CancellationToken, but I don't see mych added value there (it's not used in the code). And maybe that it's an extension method, but that's also not used in the code (it's called like var task = TaskExtensions.WhenAllSynchronousExecution(tasks);)
But this colleague is quite "sensitive". So I need to pick my battles. So is this just a custom,meagre recreation of Task.WhenAll<TResult>?
Nope, there are quite a lot of differences between the Task.WhenAll and the WhenAllSynchronousExecution. Here are some of them:
The Task.WhenAll materializes immediately the IEnumerable<Task<TResult>> sequence, by enumerating it and storing the tasks in an array. This ensures that all the tasks are up and running, before attaching continuations on them. The WhenAllSynchronousExecution doesn't do that, so it could potentially start each task after the completion of the previous task (sequentially), depending on how the enumerable sequence is implemented.
The Task.WhenAll doesn't take a CancellationToken, so when it completes you can be 100% sure that all the tasks have completed. It is always possible to fire-and-forget a Task.WhenAll task, just like any other Task, with the WaitAsync method (.NET 6), so there is no reason to bake this functionality in every async method.
The Task.WhenAll propagates the exceptions directly, as the InnerExceptions of the Task.Exception property. The WhenAllSynchronousExecution wraps the exceptions in an additional AggregateException. So you can't switch between the two APIs without rethinking your exception-handling code.
The Task.WhenAll completes as Canceled when none of the tasks is Faulted, and at least one is Canceled. The WhenAllSynchronousExecution propagates the cancellation as error, always.
The WhenAllSynchronousExecution captures the SynchronizationContext in its internal await points, so it might cause a deadlock if the caller blocks on async code.
The Task.WhenAll checks for null task instances synchronously. The WhenAllSynchronousExecution does the check during the loop that awaits each task.
Correction: Actually the integrated CancellationToken is a useful feature, that has been requested on GitHub.
I think the key to figuring out if this method has any reason to exist is to confirm if the tasks are started before the method is called.
If the tasks are not started then the method should probably be called ~WhenAllSequential.
If the tasks are started then this method seems not to add anything good.
In any case, I would argue against having this method as an extension method:
Having this method shown in auto-completion hints will sooner or later make someone pass already started tasks and think the tasks will be executed sequentially but it's the caller of this method who is in charge when the tasks are started.
Even the official WhenAll is not an extension method.

Continuation doesn't run if previous is Canceled

Hopefully, this is a simple question. I have this line of code:
Task operation = Task.Factory.StartNew(() => this.Start(arg), m_token.Token)
.ContinueWith((previous) => Completed(previous, arg), TaskScheduler.FromCurrentSynchronizationContext());
The arg object contains the CancellationToken.
If I cancel the task (which I confirmed that it is cancelled) the Completed method is not called at all. Not sure what is happening here and what did I do wrong.
Perhaps this is important bit: I'm using Enterprise library Exception handling block which wraps all exceptions. However everything works just fine when Start throws exception; the Completed is called.
The returned Task will not be scheduled for execution until the
current task has completed. If the criteria specified through the
continuationOptions parameter are not met, the continuation task will
be canceled instead of scheduled.
See details here.

Who canceled my Task?

My C# Task is getting canceled, but not by me. I don't get a stacktrace and I can't figure out where the problem occurs.
My task invocation looks like this:
var t = Task<Boolean>.Factory.StartNew(() =>
{
Boolean bOk = DoSomthingImportant();
return bOk;
}, TaskCreationOptions.AttachedToParent)
.ContinueWith<Boolean>((theTask) =>
{
var reason = theTask.IsCanceled ? "it was canceled" : "it faulted";
Debug.WriteLine("Error: Task ended because " + reason + ".");
... log the exception to one of my objects...
return false;
}, TaskContinuationOptions.NotOnRanToCompletion);
I want the continuation task to run if the task faulted or was canceled, but not if it ran okay.
The continuation is never executed.
Later on my program catches an AggregateException which is wrapping a TaskCanceledException.
My other major interaction with my tasks is to call WaitAny(taskArray, timeout) until I have no more tasks to start, then call WaitAll with no timeout until the last task is done.
Could WaitAny with a timeout cause a cancellation? Why didn't my continuation get called?
This is only my second brush with the Task library, so I am clueless.
UPDATE:
I found this SO question: How to propagate a Task's Canceled status to a continuation task.
One error in my code above (but not the cause of the Cancelation) is that I assumed that the Continuation tasks status was the same as the original task's status. In fact you have to do some work to get the one from the other, as the other post describes.
UPDATE 2:
Brian: Thanks for the documentaion reference. I had searched high and low for alternate causes of a Task being canceled, but missed these words:
"If you are waiting on a Task that transitions to the Canceled state,
a Task (wrapped in an AggregateException) is manufactured and thrown.
Note that this exception indicates successful cancellation instead of
a faulty situation. Therefore, the Task's Exception property returns
null."
You're waiting on the continuation and since the original task ran to completion the continuation task was cancelled. This behavior is covered in the documentation.

How to stop all tasks when one is finished in c#

I have different tasks to read from different files and find a word into them. I have put them into a task array which I start with waitAny method as following :
foreach (string file in filesList)
{
files[i] = Task.Factory.StartNew(() =>
{
mySearch.Invoke(file);
});
i++;
}
System.Threading.Tasks.Task.WaitAny(files);
I would like to stop all other tasks as soon as one of the tasks finishes (it finishes when it founds the word). For the moment, with waitany, i can know when one tasks finishes, but I don't know how I could know which one has finished and how to stop other tasks.
What would be the best way to achieve this ?
You can use single CancellationToken which all tasks will share. Inside mySearch.Invoke method verify value of token.IsCancellationRequested to cancel task. When some task will be finished cancel others via CancellationTokenSource.Cancel().
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
foreach (string file in filesList)
{
// pass cancellation token to your task
files[i] = Task.Factory.StartNew(() => mySearch.Invoke(file, token), token);
i++;
}
Task.WaitAny(files);
tokenSource.Cancel();
BTW you can force token to throw OperationCanceledException when source is canceled by calling token.ThrowIfCancellationRequested()
When creating a Task you can pass a CancelationToken. Set this token when one of the tasks finishes.
This will cause remaining tasks with this token to not execute. Running tasks can receive a OperationCanceledException and stop too.
I highly suggest reading How do I cancel non-cancelable async operations? by Stephen Toub. Essentially what you need to do is cancel all of these tasks, but currently you have no mechanism to cancel them.
The ideal approach would be to create a CancellationTokenSource before the foreach, pass the CancellationToken from that source to each of the child tasks, check that token periodically and stop doing work when you notice it's indicated cancellation. You can then cancel the token source in the WhenAny continuation.
If that's not an option you need to decide if it's important to actually stop the tasks (which, really, just can't be done) or if you just need to continue on with your code without waiting for them to finish (that's easy enough to do).

Does Task.Wait(int) stop the task if the timeout elapses without the task finishing?

I have a task and I expect it to take under a second to run but if it takes longer than a few seconds I want to cancel the task.
For example:
Task t = new Task(() =>
{
while (true)
{
Thread.Sleep(500);
}
});
t.Start();
t.Wait(3000);
Notice that after 3000 milliseconds the wait expires. Was the task canceled when the timeout expired or is the task still running?
Task.Wait() waits up to specified period for task completion and returns whether the task completed in the specified amount of time (or earlier) or not. The task itself is not modified and does not rely on waiting.
Read nice series: Parallelism in .NET, Parallelism in .NET – Part 10, Cancellation in PLINQ and the Parallel class by Reed Copsey
And: .NET 4 Cancellation Framework / Parallel Programming: Task Cancellation
Check following code:
var cts = new CancellationTokenSource();
var newTask = Task.Factory.StartNew(state =>
{
var token = (CancellationToken)state;
while (!token.IsCancellationRequested)
{
}
token.ThrowIfCancellationRequested();
}, cts.Token, cts.Token);
if (!newTask.Wait(3000, cts.Token)) cts.Cancel();
If you want to cancel a Task, you should pass in a CancellationToken when you create the task. That will allow you to cancel the Task from the outside. You could tie cancellation to a timer if you want.
To create a Task with a Cancellation token see this example:
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var t = Task.Factory.StartNew(() => {
// do some work
if (token.IsCancellationRequested) {
// Clean up as needed here ....
}
token.ThrowIfCancellationRequested();
}, token);
To cancel the Task call Cancel() on the tokenSource.
The task is still running until you explicitly tell it to stop or your loop finishes (which will never happen).
You can check the return value of Wait to see this:
(from http://msdn.microsoft.com/en-us/library/dd235606.aspx)
Return Value
Type: System.Boolean
true if the Task completed execution within the allotted time; otherwise, false.
Was the task canceled when the timeout expired or is the task still running?
No and Yes.
The timeout passed to Task.Wait is for the Wait, not the task.
If your task calls any synchronous method that does any kind of I/O or other unspecified action that takes time, then there is no general way to "cancel" it.
Depending on how you try to "cancel" it, one of the following may happen:
The operation actually gets canceled and the resource it works on is in a stable state (You were lucky!)
The operation actually gets canceled and the resource it works on is in an inconsistent state (potentially causing all sorts of problems later)
The operation continues and potentially interferes with whatever your other code is doing (potentially causing all sorts of problems later)
The operation fails or causes your process to crash.
You don't know what happens, because it is undocumented
There are valid scenarios where you can and probably should cancel a task using one of the generic methods described in the other answers. But if you are here because you want to interrupt a specific synchronous method, better see the documentation of that method to find out if there is a way to interrupt it, if it has a "timeout" parameter, or if there is an interruptible variation of it.

Categories

Resources