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.