Reputation: 5226
Is there a difference between lambdas () => DoSomethingAsync()
and async () => await DoSomethingAsync()
when both are typed as Func<Task>
? Which one should we prefer and when?
Here is a simple console app
using System;
using System.Threading.Tasks;
namespace asyncDemo
{
class Program
{
static void Main(string[] args)
{
var demo = new AsyncDemo();
var task = demo.RunTheDemo();
task.Wait();
Console.ReadLine();
}
}
public class AsyncDemo
{
public async Task Runner(Func<Task> action)
{
Console.WriteLine(DateTime.Now.ToLongTimeString() + " Launching the action");
await action();
}
private async Task DoSomethingAsync(string suffix)
{
await Task.Delay(2000);
Console.WriteLine(DateTime.Now.ToLongTimeString() + " Done something, " + suffix);
}
public async Task RunTheDemo()
{
await Runner(() => DoSomethingAsync("no await"));
await Runner(async () => await DoSomethingAsync("with await"));
}
}
}
The output is:
09:31:08 Launching the action
09:31:10 Done something, no await
09:31:10 Launching the action
09:31:12 Done something, with await
So in RunTheDemo
, both calls to await Runner(someLambda);
appear to do the same thing with the same timing characteristics - both have the correct two-second delay.
Both lines work, so are they exactly equivalent? What is the difference between the () => DoSomethingAsync()
and async () => await DoSomethingAsync()
constructs? Which one should we prefer and when?
This is not the same question as "should I use await
in the general case", as here we are dealing with working async code, with lambdas typed as Func<Task>
which are correctly awaited inside the consuming method. The question concerns how those lambdas are declared, and what the effects of that declaration are.
Upvotes: 30
Views: 1793
Reputation: 116548
Is there a difference between lambdas declared with and without async
Yes, there's a difference. One is an async lambda and the other is just a task-returning lambda.
An async lambda is compiled into a state machine while the other doesn't so an async lambda has different exception semantics as exceptions are encapsulated in the returned task and can't be thrown synchronously.
It's exactly the same difference that exists in regular methods. For example between this async method:
async Task FooAsync()
{
await DoSomethingAsync("with await");
}
And this task-returning method:
Task FooAsync()
{
return DoSomethingAsync("no await");
}
Looking at these methods shows the differences more clearly, but as lambdas are just syntactic sugar and are actually compiled into methods that behave just the same as these.
Which one should we prefer and when?
This really depends on your taste. Using the async keyword generates a state machine which is less performant than simply returning a task. However, the exception semantics may be surprising in some cases.
Take this code for example:
Hamster hamster = null;
Func<Task> asyncAction = () => FooAsync(hamster.Name);
var task = asyncAction();
try
{
await task;
}
catch
{
// handle
}
Would the try-catch block handle the NullReferenceException
or not?
It will not because the exception is thrown synchronously when calling asyncAction
. However the exception will be handled in this case as it is captured in the returned task and rethrown when that task is awaited.
Func<Task> asyncAction = async () => await FooAsync(hamster.Name);
I personally use task-returning lambdas for these one line expression lambdas as they're usually pretty simple. But my team, after a few extremely hurtful bugs, always use the async
and await
keywords.
Upvotes: 22
Reputation: 981
This is the output of IL Viewer for this 2 methods:
await Runner(() => DoSomethingAsync("no await"));
.method private hidebysig instance class [mscorlib]System.Threading.Tasks.Task
'<RunTheDemo>b__5_0'() cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
// [42 32 - 42 60]
IL_0000: ldarg.0 // this
IL_0001: ldstr "no await"
IL_0006: call instance class [mscorlib]System.Threading.Tasks.Task TestClass::DoSomethingAsync(string)
IL_000b: ret
} // end of method CompanyManagementController::'<RunTheDemo>b__5_0'
await Runner(async () => await DoSomethingAsync("with await"));
.method private hidebysig instance class [mscorlib]System.Threading.Tasks.Task
'<RunTheDemo>b__5_1'() cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type)
= (
01 00 45 57 65 62 43 61 72 64 2e 43 6f 6e 74 72 // ..TestClass
6f 6c 6c 65 72 73 2e 43 6f 6d 70 61 6e 79 4d 61 // +<<RunTheDemo>
6e 61 67 65 6d 65 6e 74 43 6f 6e 74 72 6f 6c 6c // b__5_1>d..
65 72 2b 3c 3c 52 75 6e 54 68 65 44 65 6d 6f 3e
62 5f 5f 35 5f 31 3e 64 00 00
)
// MetadataClassType(TestClass+<<RunTheDemo>b__5_1>d)
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()
= (01 00 00 00 )
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 2
.locals init (
[0] class TestClass/'<<RunTheDemo>b__5_1>d' V_0,
[1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder V_1
)
IL_0000: newobj instance void TestClass/'<<RunTheDemo>b__5_1>d'::.ctor()
IL_0005: stloc.0 // V_0
IL_0006: ldloc.0 // V_0
IL_0007: ldarg.0 // this
IL_0008: stfld class TestClass TestClass/'<<RunTheDemo>b__5_1>d'::'<>4__this'
IL_000d: ldloc.0 // V_0
IL_000e: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
IL_0013: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TestClass/'<<RunTheDemo>b__5_1>d'::'<>t__builder'
IL_0018: ldloc.0 // V_0
IL_0019: ldc.i4.m1
IL_001a: stfld int32 TestClass/'<<RunTheDemo>b__5_1>d'::'<>1__state'
IL_001f: ldloc.0 // V_0
IL_0020: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TestClass/'<<RunTheDemo>b__5_1>d'::'<>t__builder'
IL_0025: stloc.1 // V_1
IL_0026: ldloca.s V_1
IL_0028: ldloca.s V_0
IL_002a: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class TestClass/'<<RunTheDemo>b__5_1>d'>(!!0/*class TestClass/'<<RunTheDemo>b__5_1>d'*/&)
IL_002f: ldloc.0 // V_0
IL_0030: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TestClass/'<<RunTheDemo>b__5_1>d'::'<>t__builder'
IL_0035: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
IL_003a: ret
} // end of method CompanyManagementController::'<RunTheDemo>b__5_1'
So second one is using async state machine
Upvotes: 4
Reputation: 63742
Yes, they are the same, but this is a rather simple example. The two are functionally equivalent, you're just (possibly, depending on the compiler) doing more work when using async
.
A better case to see why async
lambda's are useful is if you need to deal with a sequence of asynchronous operations - that's what await
is for, after all:
await Runner(async () => await DoSomethingAsync(await httpClient.Get("www.google.com")));
Upvotes: 3