I read the documentation says to set status to Canceled requires three conditions:
OperationCanceledException
(or its derived exception type such as TaskCanceledException
) is throwntoken.IsCancellationRequested
is truetoken
in delegate passed to OperationCanceledException
is identical to token
passed as parameter on task creationHowever the following example throws directly without any token but succeed the cancellation. Is it because the token is a struct so the default value always satisfies the last two conditions? I am not confident about this thought, hopefully to get some explaination if I were wrong
var task = Task.Run(() =>
{
throw new OperationCanceledException();
});
try
{
task.Wait();
}
catch (AggregateException)
{
Console.WriteLine(task.Status); // Cancelled
}
It's because the compiler has chosen the overload of Task.Run
that is meant for async
lambdas such as
Task.Run(async() => await Task.Delay(100));
This has surprising behavior in your case however, because you are supposed to test vanilla Task
behavior and not that of async
methods.
// Expected behavior (note that we use action to choose the overload)
var task = Task.Run(action:() => {
throw new OperationCanceledException();
});
try {
await task;
} catch (OperationCanceledException ex) {
Console.WriteLine(task.Status); // Faulted
}
// Unexpected behavior for Task, but expected for async methods
task = Task.Run(function:() => {
throw new OperationCanceledException();
});
try {
await task;
} catch (OperationCanceledException ex) {
Console.WriteLine(task.Status); // Cancelled
}
Тhe function
overload is wrapping your original task and has logic such as this to process it and give you a "processed task":
case TaskStatus.Faulted:
{
List<ExceptionDispatchInfo> exceptionDispatchInfos = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo exceptionDispatchInfo;
if (lookForOce && exceptionDispatchInfos.Count > 0 && (exceptionDispatchInfo = exceptionDispatchInfos[0]) != null)
{
OperationCanceledException ex = exceptionDispatchInfo.SourceException as OperationCanceledException;
if (ex != null)
{
result = TrySetCanceled(ex.CancellationToken, exceptionDispatchInfo);
break;
}
}
result = TrySetException(exceptionDispatchInfos);
break;
}
Basically it converts a Faulted
inner task to a Cancelled
outer task if the exception was OperationCanceledException
.
If you want to experiment and test things, make sure to choose the first overload with action
- where such magic is not happening:
var cts = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
task = Task.Run(action:() => {
throw new OperationCanceledException(); // Faulted - no match your case
cts.Cancel();
cts2.Cancel();
//cts2.Token.ThrowIfCancellationRequested(); // Faulted - mismatch of tokens
//cts.Token.ThrowIfCancellationRequested(); // Cancelled - match tokens
//throw new OperationCanceledException(cts.Token); // Cancelled Manual throw - matching
}, cts.Token);
try {
await task;
} catch (OperationCanceledException ex) {
Console.WriteLine(task.Status);
}