Есть ли разница между лямбдами, объявленными с и без 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>
, которые правильно ожидаются внутри метода потребления. Вопрос касается того, как объявляются эти лямбды, и каковы последствия этой декларации.
Ответы
Ответ 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")));