Who canceled my Task? - c#

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.

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.

A canceled task propagates two different types of exceptions, depending on how it is waited. Why?

I encountered a strange behavior while writing some complex async/await code. I managed to create accidentally a canceled Task with a dual (schizophrenic) identity. It can either throw a TaskCanceledException or an OperationCanceledException, depending on how I wait it.
Waiting it with Wait throws an AggregateException, that contains a TaskCanceledException.
Waiting it with await throws an OperationCanceledException.
Here is a minimal example that reproduces this behavior:
var canceledToken = new CancellationToken(true);
Task task = Task.Run(() =>
{
throw new OperationCanceledException(canceledToken);
});
try { task.Wait(); } // First let's Wait synchronously the task
catch (AggregateException aex)
{
var ex = aex.InnerException;
Console.WriteLine($"task.Wait() failed, {ex.GetType().Name}: {ex.Message}");
}
try { await task; } // Now let's await the same task asynchronously
catch (Exception ex)
{
Console.WriteLine($"await task failed, {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine($"task.Status: {task.Status}");
Output:
task.Wait() failed, TaskCanceledException: A task was canceled.
await task failed, OperationCanceledException: The operation was canceled.
task.Status: Canceled
Try it on Fiddle.
Can anyone explain why is this happening?
P.S. I know that the TaskCanceledException derives from the OperationCanceledException. Still I don't like the idea of exposing an async API that demonstrates such a weird behavior.
Variants: The task below has a different behavior:
Task task = Task.Run(() =>
{
canceledToken.ThrowIfCancellationRequested();
});
This one completes in a Faulted state (instead of Canceled), and propagates an OperationCanceledException with either Wait or await.
This is quite puzzling because the CancellationToken.ThrowIfCancellationRequested method does nothing more than throwing an OperationCanceledException, according to the source code!
Also the task below demonstrates yet another different behavior:
Task task = Task.Run(() =>
{
return Task.FromCanceled(canceledToken);
});
This task completes as Canceled, and propagates a TaskCanceledException with either Wait or await.
I have no idea what's going on here!
Summarising multiple comments, but:
Task.Wait() is a legacy API that pre-dates await
for historic reasons, .Wait() would manifest cancellation as TaskCanceledException; to preserve backwards compatibility, .Wait() intervenes here, to expose all OperationCanceledException faults as TaskCanceledException, so that existing code continues to work correctly (in particular, so that existing catch (TaskCanceledException) handlers continue to work)
await uses a different API; .GetAwaiter().GetResult(), which behaves more like you would expect and want (although it is not expected to be used until the task is known to have completed), BUT!
in reality, you should almost never use .Wait() or .GetAwaiter().GetResult(), preferring await in almost all cases - see "sync over async"
if you're consuming an async API, and you choose (for whatever reason) to use .Wait() or GetAwaiter().GetResult(), then you are stepping into danger, and any consequences are now entirely your fault as the consumer; this is not something that a library author can, or should, compensate for (other than providing twin synchronous and asynchronous APIs)
in particular, note that while you might get away with subverting the awaiter API with Task[<T>], this pattern with various other awaitables would be an undefined behaviour (to be honest, I'm not sure it is really "defined" for Task[<T>])
equally: any deadlocks caused by "sync over async" (usually sync-context related) are entirely the problem of the consumer invoking a synchronous wait on an awaitable result
if in doubt: await (but equally, only await once; using await multiple times is also an undefined behaviour for awaitables other than Task[<T>])
for your exception handling: prefer catch (OperationCanceledException) over catch (TaskCanceledException), since the former will handle both via inheritance

Task doesn't Go to Faulted State If ConfigurAwait set to False

So here is what I'm trying to achieve. I launch a task and don't do wait/result on it. To ensure that if launched task goes to faulted state (for e.g. say throw an exception) I crash the process by calling Environment FailFast in Continuation.
How the problem I'm facing is that If I ran below code, Inside ContinueWith, the status of the task (which threw exception) shows up as "RanToCompletion". I expected it to be Faulted State.
private Task KickOfTaskWorkAsync()
{
var createdTask = Task.Run(() => this.RunTestTaskAsync(CancellationToken.None).ConfigureAwait(false), CancellationToken.None);
createdTask.ContinueWith(
task => Console.WriteLine("Task State In Continue with => {0}", task.Status));
return createdTask;
}
private async Task RunTestTaskAsync(CancellationToken cancellationToken)
{
throw new Exception("CrashingRoutine: Crashing by Design");
}
This is really strange :( If I remove the 'ConfigureAwait(false)' inside Task.Run function call, the task does goes to Faulted state inside Continue with. Really at loss to explain what's going on and would appreciate some help from community.
[Update]: My colleague pointed out an obvious error. I am using ConfigureAwait while I make a call to RunTestAsync inside Test.Run even though I don't await it. In this case, ConfigureAwait doesn't return a Task to Task.Run. If I don't call ConfigureAwait, a Task does get returned and things work as expected.
Your error is a specific example of a broader category of mistake: you are not observing the Task you actually care about.
In your code example, the RunTestTaskAsync() returns a Task object. It completes synchronously (because there's no await), so the Task object it returns is already faulted when the method returns, due to the exception. Your code then calls ConfigureAwait() on this faulted Task object.
But all of this happens inside another Task, i.e. the one that you start when you call Task.Run(). This Task doesn't do anything to observe the exception, so it completes normally.
The reason you observe the exception when you remove the ConfigureAwait() call has nothing to do with the call itself. If you left the call and passed true instead, you would still fail to observe the exception. The reason you can observe the exception when you remove the call is that, without the call to ConfigureAwait(), the return value of the lambda expression is a Task, and this calls a different overload of Task.Run().
This overload is a bit different from the others. From the documentation:
Queues the specified work to run on the thread pool and returns a proxy for the task returned by function.
That is, while it still starts a new Task, the Task object it returns represents not that Task, but the one returned by your lambda expression. And that proxy takes on the same state as the Task it wraps, so you see it in the Faulted state.
Based on the code you posted, I would say that you shouldn't be calling Task.Run() in the first place. The following will work just as well, without the overhead and complication of the proxy:
static void Main(string[] args)
{
Task createdTask = RunTestTaskAsync();
createdTask.ConfigureAwait(false);
createdTask.ContinueWith(
task => Console.WriteLine("Task State In Continue with => {0}", task.Status)).Wait();
}
private static async Task RunTestTaskAsync()
{
throw new Exception("CrashingRoutine: Crashing by Design");
}
(I removed the CancellationToken values, because they have nothing at all to do with your question and are completely superfluous here.)

Is there any other way to set Task.Status to Cancelled

Ok, so I understand how to do Task cancellations using CancellationTokenSource. it appears to me that the Task type "kind of" handles this exception automatically - it sets the Task's Status to Cancelled.
Now you still actually have to handle the OperationCancelledException. Otherwise the exception bubbles up to Application.UnhandledException. The Task itself kind of recognizes it and does some handling internally, but you still need to wrap the calling code in a try block to avoid the unhandled exception. Sometimes, this seems like unnecessary code. If the user presses cancel, then cancel the Task (obviously the task itself needs to handle it too). I don't feel like there needs to be any other code requirement. Simply check the Status property for the completion status of the task.
Is there any specific reason for this from a language design point of view? Is there any other way to set the Status property to cancelled?
You can set a Task's status to cancelled without a CancellationToken if you create it using TaskCompletionSource
var tcs = new TaskCompletionSource();
var task = tcs.Task;
tcs.SetCancelled();
Other than that you can only cancel a running Task with a CancellationToken
You only need to wrap the calling code in a try/catch block where you're asking for the result, or waiting for the task to complete - those are the situations in which the exception is thrown. The code creating the task won't throw that exception, for example.
It's not clear what the alternative would be - for example:
string x = await GetTaskReturningString();
Here we never have a variable referring to the task, so we can't explicitly check the status. We'd have to use:
var task = GetTaskReturningString();
string x = await task;
if (task.Status == TaskStatus.Canceled)
{
...
}
... which is not only less convenient, but also moves the handling of the "something happened" code into the middle of the normal success path.
Additionally, by handling cancellation with an exception, if you have several operations, you can put all the handling in one catch block instead of checking each task separately:
try
{
var x = await GetFirstTask();
var y = await GetSecondTask(x);
}
catch (OperationCanceledException e)
{
// We don't care which was canceled
}
The same argument applies for handling cancellation in one place wherever in the stack the first cancellation occurred - if you have a deep stack of async methods, cancellation in the deepest method will result in the top-most task being canceled, just like normal exception propagation.

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.

Categories

Resources