📜 ⬆️ ⬇️

Rules for working with Tasks API. Part 1

Almost 6 years have passed since the appearance of the task in .NET. However, I still see some confusion when using Task. Run () and Task.Factory. StartNew () in the project code. If this can be attributed to their similarity, then some problems may arise due to dynamic in C #.

In this post I will try to show the problem, the solution and the origins.

Problem


Suppose we have a code that looks like this:
')
static async Task<dynamic> Compute(Task<dynamic> inner) { return await Task.Factory.StartNew(async () => await inner); } 

Question to experts : is there a problem in this example? If so, which one? The code is compiled, the return type of Task is in place, the async modifier when using await is also.

Do you think this is a missed ConfigureAwait? Haha

NB : I’ll leave off the ConfigureAwait question, because there’s another article.

Origins


Before the async / await idiom, the main use of the Tasks API was Task.Factory. StartNew () with lots of overloads. So, Task. Run () makes this approach a bit easier by omitting the scheduler (TaskScheduler), etc.

 static Task<T> Run<T>(Func<T> inner) { return Task.Run(inner); } static Task<T> RunFactory<T>(Func<T> inner) { return Task.Factory.StartNew(inner); } 

There is nothing especially in the example above, but this is where the differences begin, and the main problem arises - many are starting to think that Task.Run () is a lightweight Task.Factory.StartNew ().

However, it is not!

To make it clearer, consider an example:

 static Task<T> Compute<T>(Task<T> inner) { return Task.Run(async () => await inner); } static async Task<T> ComputeWithFactory<T>(Task<T> inner) { return await await Task.Factory.StartNew(async () => await inner); } 

What? Two await'a? Exactly.

It's all about overload:

 public static Task<TResult> Run<TResult>(Func<Task<TResult>> function) { // code } public Task<TResult> StartNew<TResult>(Func<TResult> function) { // code } 

Despite the fact that the return type of both methods is Task <TResult> , the Run input parameter is Func <Task <TResult >> .

In the case of async () => await inner Task.Run will receive a ready - made state machine (and we know that await is nothing more than the transformation of code into a state machine), where everything turns into Task.
StartNew will get the same thing, but TResult will already be Task <Task <T >> .

- OK, but why the original example does not fall with a compilation error, since missing second await ?
The answer is: dynamic .

In one article , I already described the work of dynamic : each statement in C # turns into a call node (call-site), which refers to the time of execution, not compilation. At the same time, the compiler itself tries to transfer more metadata to runtime.

The Compute () method uses and returns Task <dynamic> , which causes the compiler to create these same call nodes.
Moreover, this is the correct code - the result in runtime will be Task <Task <dynamic >> .

Decision


It is very simple: you must use the Unwrap () method.

In the code without dynamic instead of two awaits you can do one thing:

 static async Task<T> ComputeWithFactory<T>(Task<T> inner) { return await Task.Factory.StartNew(async () => await inner).Unwrap(); } 

And apply to

 static async Task<dynamic> Compute(Task<dynamic> inner) { return await Task.Factory.StartNew(async () => await inner).Unwrap(); } 

Now, as expected, the result will be Task <dynamic> , where dynamic is exactly the return value of the inner'a, but not another task.

findings


Always use the Unwrap extension method for Task.Factory.StartNew (). This will make your code more idiomatic (one await per call) and will not allow the tricks of dynamic.

Task.Run () - for ordinary calculations.
Task.Factory.StartNew () + Unwrap () - for normal calculations with an indication of TaskScheduler'a, etc.

Source: https://habr.com/ru/post/280234/


All Articles