В чем разница между следующими подходами Func<Task> асинхронного делегирования?

#c# #async-await

#c# #асинхронное ожидание

Вопрос:

Если у меня есть следующий метод:

 public async Task<T> DoSomethingAsync<T>(Func<Task<T>> action)
{
   // bunch of async code..then "await action()"
}
  

В чем разница между следующими двумя способами использования:

 public async Task MethodOneAsync()
{
   return await DoSomethingAsync(async () => await SomeActionAsync());
}

public async Task MethodTwoAsync()
{
   return await DoSomethingAsync(() => SomeActionAsync());
}
  

Оба компилируются, оба работают, и предупреждений C # нет.

В чем разница (если есть)? Будут ли оба метода выполняться асинхронно, если их ожидает вызывающий?

Комментарии:

1. Разница в том, что MethodTwo элиды async-await и MethodOne нет. Не тратьте время на споры о «разных» мнениях ;). У Стивена Клири есть статья об этом различии, исключающем Async и Await

Ответ №1:

Между ними нет функциональной разницы. Единственная разница заключается Task в том, возвращается ли from SomeActionAsync напрямую или если оно ожидается. У Стивена Клири есть хороший пост в блоге об этом, и он рекомендует второй подход для этого тривиального случая.

Причина, по которой доступен первый подход, заключается в том, что у вас может быть нетривиальное лямбда-выражение, подобное этому:

 public async Task MethodOneAsync()
{
    return await DoSomethingAsync(async () => {
        var i = _isItSunday ? 42 : 11;
        var someResult = await SomeActionAsync(i);
        return await AnotherActionAsync(someResult*i);
    });
}
  

Таким образом, разница такая же, как разница между методом с этой сигнатурой public async Task<int> MyMethod и этим public Task<int> MyMethod

Комментарии:

1. Что мешает пользователю перенести логику фигурных скобок в отдельный метод и использовать Async Await , это не причина, по которой здесь предоставляется async await , это полностью уменьшает значение Async-Await . Цель Async — Await — это всегда неблокирующая операция, при которой длительная операция может выполняться в фоновом режиме, поскольку OP подтвердил, что функционально два одинаковы, и они действительно есть, разница будет только в производительности для продолжительного метода, и именно поэтому предпочтительнее Async Await

2. Также позвольте мне добавить, что асинхронная операция на основе вычислений всегда выполняется в threadpool даже с Async -Await, другого способа нет, просто основной вызывающий поток свободен. Подлинная асинхронная операция — это в основном операции ввода-вывода, поскольку они не требуют никакого потока и действительно выполняются в фоновом режиме

3. итак, ответ, который не ожидает, проглотит исключение, и только внешний код должен будет обрабатывать? нужен кто-нибудь, чтобы просто перефразировать, что такое разница в двух словах.

4. @Seabizkit Какое исключение?

5. @JoakimM.H. read blog.stephencleary.com/2016/12/eliding-async-await.html

Ответ №2:

Короткий ответ

MethodOneAsync() действительно асинхронный и должен использоваться, но MethodTwoAsync() не является действительно асинхронным, поскольку он вызывает поток пула потоков

Длинный ответ

С целью тестирования и запуска я упростил ваш код следующим образом:

Выполнение из основного метода Linqpad следующим образом:

 var resultTask = MethodOneAsync(); // Comment one the methods

resultTask.Result.Dump();
  

Фактический код

 public async Task<int> DoSomethingAsync(Func<Task<int>> action)
{
    return await Task.FromResult<int>(3);
}

public async Task<int> MethodOneAsync()
{
    await Task.Delay(10);
    return await DoSomethingAsync(async () => await Task.FromResult<int>(3));
}

public async Task<int> MethodOneAsync()
{
    await Task.Delay(10);
    return await DoSomethingAsync(() => Task.FromResult<int>(3));
}
  

Теперь я рассмотрел разницу IL generated между двумя вызовами, и следующее является наиболее важным отличием:

Первый вызов с Async and Await inside DoSomethingAsync имеет следующий IL:

 <>c.<MethodOneAsync>b__2_0:
IL_0000:  newobj      UserQuery <>c <<MethodOneAsync>b__2_0>d..ctor
IL_0005:  stloc.0     
IL_0006:  ldloc.0     
IL_0007:  ldarg.0     
IL_0008:  stfld       UserQuery <>c <<MethodOneAsync>b__2_0>d.<>4__this
IL_000D:  ldloc.0     
IL_000E:  call        System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Int32>.Create
IL_0013:  stfld       UserQuery <>c <<MethodOneAsync>b__2_0>d.<>t__builder
IL_0018:  ldloc.0     
IL_0019:  ldc.i4.m1   
IL_001A:  stfld       UserQuery <>c <<MethodOneAsync>b__2_0>d.<>1__state
IL_001F:  ldloc.0     
IL_0020:  ldfld       UserQuery <>c <<MethodOneAsync>b__2_0>d.<>t__builder
IL_0025:  stloc.1     
IL_0026:  ldloca.s    01 
IL_0028:  ldloca.s    00 
IL_002A:  call        System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Int32>.Start<<<MethodOneAsync>b__2_0>d>
IL_002F:  ldloc.0     
IL_0030:  ldflda      UserQuery <>c <<MethodOneAsync>b__2_0>d.<>t__builder
IL_0035:  call        System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Int32>.get_Task
IL_003A:  ret      
  

Второй без Async and Await имеет следующий код:

 <>c.<MethodOneAsync>b__2_0:
IL_0000:  ldc.i4.3    
IL_0001:  call        System.Threading.Tasks.Task.FromResult<Int32>
IL_0006:  ret      
  

Помимо этого, первый имеет полный конечный машинный код для дополнительного async await вызова, который ожидается.

Важные моменты:

  1. Для использования асинхронного вызова метода async () => await SomeActionAsync() , поскольку это истинное асинхронное выполнение и работает на портах завершения ввода-вывода
  2. В другом случае он вызывает Threadpool поток для выполнения асинхронного метода, что не подходит для асинхронного выполнения

Я могу вставить полный IL, если требуется, чтобы понять разницу, но лучше всего оценить то же самое в Visual studio или LINQPad, чтобы понять нюансы

Комментарии:

1. @StephenCleary есть ли шанс, что вы могли бы прокомментировать этот ответ, пожалуйста?

2. @Pure.Krome, спасибо за ваш комментарий, голосование против никогда не является проблемой, но важно обоснование, чтобы я мог учиться и исправлять. Это никогда не должно быть причудливым.

3. Разница не в том, задействован или нет поток threadpool. Единственная разница заключается в том, возвращается ли задача из SomeActionAsync() напрямую или если она ожидается. Смотрите этот пост в блоге Стивена Клири.

4. Извините, но это неправильно. Независимо от того, является ли аргумент lambda для DoSomethingAsync асинхронным или нет, не имеет никакого отношения к блокировке или неблокированию. Давайте предположим, что DoSomethingAsync и SomeActionAsync являются правильно реализованными асинхронными методами: разница в том, возвращает ли лямбда-выражение задачу из SomeActinAsync напрямую или оборачивает результат в новую задачу и возвращает эту новую задачу. Сгенерированный IL отличается тем, что async lambda создает избыточный конечный автомат.

5. Difference is in original example one of the implementation doesn't await the task within Func, which means calling thread is never relieved as expected in Async and Func will return only on completion of Task, thus blocking calling thread. Это просто не так, как это работает. Задача просто возвращается. Чтобы заблокировать, вам нужно будет сделать что-то вроде SomeActionAsync().Result .

Ответ №3:

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

 public async Task MethodOneAsync()
{
    return await DoSomethingAsync(() => SomeActionAsync());
}
  

Во-вторых, это не имеет ничего общего с «истинной» асинхронностью, поскольку это было бы деталью реализации, которая не показана

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

 await DoSomethingAsync(async () => await SomeActionAsync());
  

и

 await DoSomethingAsync(() =>  SomeActionAsync()); 
  

Учитывая определение DoSomethingAsync , в первом примере компилятор создаст дополнительную IAsyncStateMachine реализацию вместо простой пересылки Task . Т.е. Больше скомпилированного кода, больше IL, больше инструкций, и в этом примере, по-видимому, избыточно.

Однако, поскольку это всего лишь простой проход, нет другого кода, который будет выдавать, следовательно, дополнительный конечный автомат или try catch и Task.FromException не нужен.

Реальные заметные различия возникли бы, если бы ваша подпись на самом деле была Action вместо Func<Task> того, чтобы создавать async void заданную асинхронную лямбду, однако в вашем вопросе это не так.

Комментарии:

1. Ваш первый комментарий считает, что вы ошибаетесь в ожидании «void», вы всегда должны возвращать task, даже если это void . (я могу ошибаться). Ваш ответ пока самый интересный. Все еще не понимаю, в чем разница, кроме, возможно, другой оболочки task. Возможно, вы нашли что-нибудь еще, объясняющее разницу более подробно, кажется очень странным. это похоже на то, что вы передаете await как часть делегата, но это тоже не имеет особого смысла.

Ответ №4:

Конструкция Async / await вставляет некоторый код инфраструктуры, который полезен только в том случае, если после «await» есть какой-то код. В противном случае это, по сути, ничего не делает. Ваш код эквивалентен

 public Task MethodThreeAsync()
{
    return DoSomethingAsync(() => SomeActionAsync());
}
  

Все три метода являются «истинно асинхронными».

Комментарии:

1. Этот инфраструктурный код, сгенерированный await вызовом, также обеспечивает фрейм стека в отчетах об исключениях. Если вы полагаетесь Exception.StackTrace на отладку, вам следует использовать await , а не просто возвращать задачи.

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

3. Не уверен, почему вы проголосовали против. Это в основном правильно, за исключением некоторых ошибок, связанных с обработкой ошибок.