Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are the await / async keywords needed under Task.Run?

I have an async lambda expression wrapped under Task.Run. However, it looks like I can drop the async and await keywords and they will produce the same result.

t1 = Task.Run(() => DoSomethingExpensiveAsync());
t2 = Task.Run(() => DoSomethingExpensiveAsync());
await Task.WhenAll(t1, t2);

vs

var t1 = Task.Run(async () => await DoSomethingExpensiveAsync());
var t2 = Task.Run(async () => await DoSomethingExpensiveAsync());
await Task.WhenAll(t1, t2);
  1. How come the compiler let me do this and what is happening behind the scene?
  2. Is there a situation where adding them will make a difference?
like image 239
little_stone_05 Avatar asked Oct 15 '22 06:10

little_stone_05


People also ask

Does Task Run Run async?

Unlike StartNew , Task. Run is async -aware.

Do I need to use await when calling an async function?

Use of async and await enables the use of ordinary try / catch blocks around asynchronous code. Note: The await keyword is only valid inside async functions within regular JavaScript code. If you use it outside of an async function's body, you will get a SyntaxError .

Where can we use the await keyword?

Asynchronous programming with async and await follows the task-based asynchronous pattern. You can use the await operator only in a method, lambda expression, or anonymous method that is modified by the async keyword.

What happens if we execute an asynchronous method but don't await it?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.


2 Answers

There are actually three variants.

var task = Task.Run(() => DoSomethingExpensiveAsync());

^ This one declares a new anonymous non-async function that calls DoSomethingExpensiveAsync() and returns its Task. The compiler compiles this anonymous function and passes it as an argument to Task.Run().

var task = Task.Run( async () => await DoSomethingExpensiveAsync() );

^ This one declares a new anonymous async function that calls DoSomethingExpensiveAsync(). It then returns an incomplete Task, waits for DoSomethingExpensiveAsync() to finish, and then signals the task as complete.

var task = Task.Run(DoSomethingExpensiveAsync);

^ This one does not declare a new anonymous function at all. A direct reference to DoSomethingExpensiveAsync will be passed as an argument to Task.Run().

All of these are valid because all three versions return a Task and therefore match the overload of Task.Run() that accepts a Func<Task>.

As a black box, all three calls will end up doing the same thing. However the first two result in a new function being compiled (although I'm not certain it wouldn't be optimized away) and the second one also results in another state machine being created for it.

The difference might be clearer if we rewrite them without using lambda expressions or anonymous functions. The following code does exactly the same thing:

//This is the same as Task.Run( () => DoSomethingExpensiveAsync());
Task Foo()
{
    return DoSomethingExpensiveAsync();
}
var task = Task.Run(Foo);

//This is the same as Task.Run(async () => await DoSomethingExpensiveAsync());
async Task Bar()
{
    return await DoSomethingExpensiveAsync();
}
var task = Task.Run(Bar);

The difference between these two is that one "elides" tasks while the other doesn't. Stephen Cleary has written a whole blog on the subject.

like image 105
John Wu Avatar answered Oct 20 '22 12:10

John Wu


How come the compiler let me do this and what is happening behind the scene?

The overload of Task.Run that you're invoking takes a Func<Task> - that is, a Task-returning function. It doesn't matter where the Task comes from; the function just needs to return it from somewhere.

If you pass a delegate without async and await, then the delegate is just calling a Task-returning function and returns that same Task. If you pass a delegate with async and await, then the delegate calls the Task-returning function and awaits it; the actual Task returned from the delegate is created by the async keyword.

In this case, the two are semantically equivalent. Using the async/await keywords are a bit less efficient, since the compiler creates a state machine for the async delegate.

Is there a situation where adding them will make a difference?

Yes. In the general case, you should keep async and await. Only remove them in extremely simple "passthrough" situations like the one here.

like image 38
Stephen Cleary Avatar answered Oct 20 '22 12:10

Stephen Cleary