Anthony
Anthony

Reputation: 5226

Is there a difference between lambdas declared with and without async

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

Answers (3)

i3arnon
i3arnon

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

Jacob Sobus
Jacob Sobus

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

Luaan
Luaan

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

Related Questions