little_stone_05
little_stone_05

Reputation: 155

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?

Upvotes: 6

Views: 588

Answers (4)

John Wu
John Wu

Reputation: 52210

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.

Upvotes: 5

Stephen Cleary
Stephen Cleary

Reputation: 456322

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.

Upvotes: 1

Paulo Morgado
Paulo Morgado

Reputation: 14836

Without knowing what DoSomethingExpensiveAsync it's impossible to tell for certain what will happen.

Let's assume DoSomethingExpensiveAsync is something like this:

async Task DoSomethingExpensiveAsync()
{
     SynchronousMethod();
     await AsynchronousMethod();
}

In the first snippet, Task.Run will schedule the invocation of DoSomethingExpensiveAsync to the thread pool and returns as soon as SynchronousMethod returns.

In the second snippet, Task.Run will schedule the invocation of DoSomethingExpensiveAsync to the thread pool and returns when the Task return by DoSomethingExpensiveAsync is completed.

Upvotes: -1

Sir Rufo
Sir Rufo

Reputation: 19096

Your code is the same as

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

because Task.Run( Func< function ) will return a proxy of the task generated by function. There is no other Task created, and so you are awaiting the original tasks.

When you already have an async method then there is no need to use Task.Run at all.

Upvotes: 0

Related Questions