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.
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 C# application which executes a method that returns a Task. In this case, I'm trying to show a message box containing the exception details if the called method throws an exception.
If I call it like this, I can see the exception fine in task.Exception:
MyClass.MyAsyncMethod(cancellationToken)
.LogExceptions()
.ContinueWith(task => {
MessageBox.Show(task.Exception);
}, cancellationToken, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
However, if I add an OnlyOnRanToCompletion continuation, task.Exception in the NotOnRanToCompletion continuation becomes null:
MyClass.MyAsyncMethod(cancellationToken)
.LogExceptions()
.ContinueWith(task => {
Log.Info("Executed");
}, cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default)
.ContinueWith(task => {
MessageBox.Show(task.Exception);
}, cancellationToken, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
The code in the OnlyOnranToCompletion continuation is not executed, but the code in the NotOnRanToCompletion continuation is executed and task.Exception is null. Why does this happen?
Note: I cannot use C# >= 5.0 features like async or await. I'm also able to get around this by having it all under a single None continuation method that checks the exception parameter of the task and determines if an exception occured. However, I'm interested in WHY the above behaviour takes place.
This is the called method:
public Task<bool> MyAsyncMethod(CancellationToken cancellationToken)
{
return Task<bool>.Factory.StartNew(() =>
{
...
try
{
var response = request.GetResponse();
if (response.StatusCode != HttpStatusCode.OK)
throw new Exception("Invalid response status code: " + response.StatusCode);
}
catch (Exception ex)
{
Logger.Error("Request failed", ex));
throw;
}
}, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
}
There is no exception because the Task that it is a continuation of didn't throw an exception. The Task that it is a continuation of (the previous call to ContinueWith) will have completed successfully.
If you want to handle both the error and non-error case, you're probably better off just having one continuation and checking if it was successful in that continuation, rather than using the continuation options. Alternatively you could store the Task from the actual work that you're doing and add both continuations as continuations to that one Task, rather than having one continuation being a continuation of the other.
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);
}
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.
I am using the new async await features to upgrade from backgroundworker in C#. In the following code I am trying to replicate the execution of multiple tasks with ContinueWith method.
Task t1 = new Task
(
() =>
{
Thread.Sleep(10000);
// make the Task throw an exception
MessageBox.Show("This is T1");
}
);
Task t2 = t1.ContinueWith
(
(predecessorTask) =>
{
if (predecessorTask.Exception != null)
{
MessageBox.Show("Predecessor Exception within Continuation");
return;
}
Thread.Sleep(1000);
MessageBox.Show("This is Continuation");
},
TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.OnlyOnRanToCompletion
);
t1.Start();
try
{
t1.Wait(); <------ Comment
t2.Wait(); <------ Comment
}
catch (AggregateException ex)
{
MessageBox.Show(ex.InnerException.Message);
}
My question is when I comment t1.wait and t2.wait Tasks are not blocking UI. However when I uncomment t1.wait and t2.wait UI blocks until thread is completed. The desired behavior is to catch errors in try/catch block without blocking UI. What Am I missing?
If this is running within a UI event handler, you can add the async modifer to the method signature and change t1.Wait() to await t1. This will return control to the UI thread, and when the Thread.Sleep has completed, the continuation will execute and any exceptions will be caught.
When you use Task.Wait(), you are basically saying "wait here my task to complete". That's why you are blocking the thread. A good way to handle exception in tasks is using the Task.ContinueWith overload and pass OnlyOnFaulted as TaskContinuationOption which would look like:
Task yourTask = new Task {...};
yourTask.ContinueWith( t=> { /*handle expected exceptions*/ }, TaskContinuationOptions.OnlyOnFaulted );
If you're going to use the Task-based Asynchronous Pattern, then you should use the recommended guidelines. I have an MSDN article describing many of them.
In particular:
Use Task.Run instead of the Task constructor with Task.Start.
Use await instead of ContinueWith.
Do not use AttachedToParent.
If you apply these changes, your code will then look like this:
try
{
await Task.Run(() =>
{
Thread.Sleep(10000);
// make the Task throw an exception
MessageBox.Show("This is T1");
});
await Task.Run(() =>
{
Thread.Sleep(1000);
MessageBox.Show("This is Continuation");
});
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}