Reputation: 155
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);
Upvotes: 6
Views: 588
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
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 await
s 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
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
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