I am throwing an OperationCanceledException
from three different Tasks and each having slight differences, as per the code below:
static async Task ThrowCancellationException()
{
throw new OperationCanceledException();
}
static void Main(string[] args)
{
var t1 = new Task(() => throw new OperationCanceledException());
t1.Start();
try { t1.Wait(); } catch { }
Task t2 = new Task(async () => throw new OperationCanceledException());
t2.Start();
try { t2.Wait(); } catch { }
Task t3 = ThrowCancellationException();
Console.WriteLine(t1.Status); // prints Faulted
Console.WriteLine(t2.Status); // prints RanToCompletion
Console.WriteLine(t3.Status); // prints Canceled
}
My question is:
Why the statuses are different for each task?
I could understand that there are differences between the code/lambdas marked with async
and the lambda not marked with async
but the status is different even between the async
lambda and the async
method running the same code.
I could understand that there are differences between the code/lambdas marked with async and the lambda not marked with async but the status is different even between the async lambda and the async method running the same code.
This isn't quite true.
If you look carefully at that new Task(async () => throw new OperationCanceledException())
, you'll see it's calling the overload new Task(Action action)
(there's no overload which takes a Func<Task>
). This means that it's equivalent to passing an async void
method, not an async Task
method.
So:
Task t2 = new Task(async () => throw new OperationCanceledException());
t2.Start();
try { t2.Wait(); } catch { }
This compiles to something like:
private static async void CompilerGeneratedMethod()
{
throw new OperationCanceledException()
}
...
Task t2 = new Task(CompilerGeneratedMethod);
t2.Start();
try { t2.Wait(); } catch { }
This grabs a thread from the ThreadPool, and runs CompilerGeneratedMethod
on it. When an exception is throw from inside an async void
method, the exception is re-thrown somewhere appropriate (in this case, it's re-thrown on the ThreadPool), but the CompilerGeneratedMethod
method itself returns straight away. This causes the Task t2
to complete straight away, which is why its status is RanToCompletion
.
So what happened to the exception? It's about to bring down your application! Stick a Console.ReadLine
at the end of your Main
, and see that the application exits before you have a chance to press enter.
This:
Task t3 = ThrowCancellationException();
is very different. It's not attempting to run anything on the ThreadPool. ThrowCancellationException
is run synchronously, and synchronously returns a Task
which contains the OperationCanceledException
. A Task
which contains an OperationCanceledException
is treated as Canceled
.
If you want to run an async
method on the ThreadPool, use Task.Run
. This has an overload which takes a Func<Task>
, which means that:
Task t2 = Task.Run(async () => throw new OperationCanceledException());
Compiles to something like:
private static async Task CompilerGeneratedMethod()
{
throw new OperationCanceledException();
}
...
Task t2 = Task.Run(CompilerGeneratedMethod);
Here, when CompilerGeneratedMethod
is executed on the ThreadPool, it returns a Task
containing the OperationCanceledException
. The Task machinery then transitions the Task t2
to the Canceled
state.
As an aside, avoid new Task
, and prefer using Task.Run
if you want to explicitly run a method on the ThreadPool. There are lots of methods in the TPL which were introduced before async/await, and are confusing when used with it.
When you create a task using the Task
constructor, you can provide a CancellationToken
as optional argument. The task will result in a Canceled
state only in case of an OperationCanceledException
that is associated with this specific CancellationToken
. Otherwise the exception will be interpreted as a fault. Here is how you can associate an OperationCanceledException
with a CancellationToken
:
var cts = new CancellationTokenSource();
var t1 = new Task(() =>
{
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}, cts.Token);
About using the Task
constructor with an async delegate as argument, the correct way to do it is by creating a nested Task<Task>
:
Task<Task> t2 = new Task<Task>(async () => throw new OperationCanceledException());
t2.Start();
try { t2.Result.Wait(); } catch { }
Console.WriteLine(t2.Result.Status); // prints Canceled
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With