Подтвердить что ты не робот

Есть ли разница между лямбдами, объявленными с и без async

Есть ли разница между lambdas () => DoSomethingAsync() и async () => await DoSomethingAsync(), если оба они напечатаны как Func<Task>? Какой из них мы предпочитаем и когда?

Вот простое консольное приложение

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"));
        }
    }
}

Вывод:

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

Итак, в RunTheDemo оба вызова await Runner(someLambda); оказываются похожими на те же самые временные характеристики - обе имеют правильную двухсекундную задержку.

Обе линии работают, точно так же они эквивалентны? В чем разница между конструкциями () => DoSomethingAsync() и async () => await DoSomethingAsync()? Какой из них мы предпочитаем и когда?

Это не тот же вопрос, как "следует использовать await в общем случае", так как здесь мы имеем дело с работающим асинхронным кодом, с lambdas, напечатанным как Func<Task>, которые правильно ожидаются внутри метода потребления. Вопрос касается того, как объявляются эти лямбды, и каковы последствия этой декларации.

4b9b3361

Ответ 1

Есть ли разница между объявлением lambdas с и без async

Да, есть разница. Один из них - асинхронный лямбда, а другой - просто возвращающая задачу лямбда.

Асинхронная лямбда скомпилирована в конечный автомат, в то время как другой не так асинхронный лямбда имеет другую семантику исключений, так как исключения инкапсулируются в возвращаемую задачу и не могут быть выбраны синхронно.

Это точно такое же различие, которое существует в обычных методах. Например, между этим методом async:

async Task FooAsync()
{
    await DoSomethingAsync("with await");
}

И этот метод возврата задачи:

Task FooAsync()
{
    return DoSomethingAsync("no await");
}

Взгляд на эти методы показывает различия более четко, но поскольку лямбды являются просто синтаксическим сахаром и фактически скомпилированы в методы, которые ведут себя так же, как эти.

Какой из них мы предпочитаем и когда?

Это действительно зависит от вашего вкуса. Использование ключевого слова async генерирует конечный автомат, который менее эффективен, чем просто возвращает задачу. Однако в некоторых случаях семантика исключений может быть удивительной.

Возьмите этот код, например:

Hamster hamster = null;
Func<Task> asyncAction = () => FooAsync(hamster.Name);

var task = asyncAction();
try
{
    await task;
}
catch
{
    // handle
}

Будет ли блок try-catch обрабатывать NullReferenceException или нет?

Это не произойдет, потому что исключение синхронно вызывается при вызове asyncAction. Однако исключение будет обрабатываться в этом случае, так как оно фиксируется в возвращаемой задаче и возвращается при ожидании этой задачи.

Func<Task> asyncAction = async () => await FooAsync(hamster.Name);

Я лично использую возвращаемые задачи lambdas для выражения одной строки lambdas, поскольку они обычно довольно просты. Но моя команда, после нескольких чрезвычайно вредных ошибок, всегда использует ключевые слова async и await.

Ответ 2

Это вывод IL Viewer для этих 2 методов:

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'

Таким образом, второй использует асинхронный конечный автомат

Ответ 3

Да, они одинаковы, но это довольно простой пример. Эти два функционально эквивалентны, вы просто (возможно, в зависимости от компилятора) делаете больше работы при использовании async.

Лучше понять, почему async lambda полезны, если вам нужно иметь дело с последовательностью асинхронных операций - для чего await, в конце концов:

await Runner(async () => await DoSomethingAsync(await httpClient.Get("www.google.com")));