Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Similar code in Tasks returning different status codes

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.

like image 900
meJustAndrew Avatar asked Mar 02 '23 20:03

meJustAndrew


2 Answers

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.

like image 104
canton7 Avatar answered Mar 09 '23 05:03

canton7


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
like image 36
Theodor Zoulias Avatar answered Mar 09 '23 07:03

Theodor Zoulias