Какова цель "возврата ждать" в C#?
Есть ли сценарий, где написание метода, как это:
public async Task<SomeResult> DoSomethingAsync()
{
// Some synchronous code might or might not be here... //
return await DoAnotherThingAsync();
}
вместо этого:
public Task<SomeResult> DoSomethingAsync()
{
// Some synchronous code might or might not be here... //
return DoAnotherThingAsync();
}
будет иметь смысл?
Зачем использовать return await
построить, когда вы можете напрямую вернуться Task<T>
изнутри DoAnotherThingAsync()
вызов?
Я вижу код с return await
во многих местах, я думаю, я должен был что-то пропустить. Но, насколько я понимаю, не использование в этом случае ключевых слов async / await и прямой возврат Задачи будет функционально эквивалентным. Зачем добавлять дополнительные накладные расходы на дополнительные await
слой?
10 ответов
Есть один хитрый случай, когда return
в обычном методе и return await
в async
метод ведет себя по-разному: в сочетании с using
(или, вообще говоря, любой return await
в try
блок).
Рассмотрим эти две версии метода:
Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return foo.DoAnotherThingAsync();
}
}
async Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return await foo.DoAnotherThingAsync();
}
}
Первый метод будет Dispose()
Foo
возражать, как только DoAnotherThingAsync()
Метод возвращает, что, вероятно, задолго до его фактического завершения. Это означает, что первая версия, вероятно, глючит (потому что Foo
утилизируется слишком рано), пока вторая версия будет работать нормально.
Если вам не нужно async
(т.е. вы можете вернуть Task
напрямую), то не используйте async
,
Есть несколько ситуаций, когда return await
полезно, например, если у вас есть две асинхронные операции:
var intermediate = await FirstAsync();
return await SecondAwait(intermediate);
Для больше на async
производительность, см. статью MSDN Стивена Туба и видео по теме.
Обновление: я написал сообщение в блоге, которое входит в гораздо более подробную информацию.
Единственная причина, по которой вы захотите это сделать, это если есть какая-то другая await
в более раннем коде, или если вы каким-то образом манипулируете результатом перед его возвратом. Другим способом, которым это могло бы случиться, является try/catch
это меняет способ обработки исключений. Если вы ничего не делаете, то вы правы, нет никаких причин добавлять накладные расходы на создание метода async
,
Если вы не используете return await, вы можете испортить трассировку стека во время отладки или когда она печатается в журналах исключений.
Когда вы возвращаете задачу, метод выполнил свою задачу, и он вышел из стека вызовов. Когда вы используете return await
вы оставляете это в стеке вызовов.
Например:
Стек вызовов при использовании await: A ожидает задачи от B => B ожидает задачи от C
Стек вызовов, когда не используется await: A ожидает задачу от C, которую B вернул.
Еще один случай, когда вам может потребоваться дождаться результата:
async Task<IFoo> GetIFooAsync()
{
return await GetFooAsync();
}
async Task<Foo> GetFooAsync()
{
var foo = await CreateFooAsync();
await foo.InitializeAsync();
return foo;
}
В этом случае, GetIFooAsync()
должен дождаться результата GetFooAsync
потому что тип T
отличается между двумя методами и Task<Foo>
не присваивается напрямую Task<IFoo>
, Но если вы ждете результата, он просто становится Foo
который напрямую присваивается IFoo
, Тогда асинхронный метод просто перепаковывает результат внутри Task<IFoo>
и понеслось.
Создание в противном случае простого метода "thunk" async создает в памяти асинхронный конечный автомат, а не асинхронный - нет. Хотя это часто может указывать людям на использование неасинхронной версии, потому что она более эффективна (что верно), это также означает, что в случае зависания у вас нет никаких доказательств того, что этот метод участвует в стеке возврата / продолжения. что иногда затрудняет понимание зависания.
Так что да, когда perf не критичен (и обычно это не так), я включу асинхронность во всех этих методах thunk, чтобы у меня был асинхронный конечный автомат, чтобы помочь мне диагностировать зависания позже, а также, чтобы гарантировать, что если те Методы thunk постоянно развиваются, и они будут возвращать ошибочные задачи вместо броска.
Это также смущает меня, и я чувствую, что предыдущие ответы упустили из виду ваш актуальный вопрос:
Зачем использовать конструкцию return await, если вы можете напрямую вернуть Task из внутреннего вызова DoAnotherThingAsync()?
Ну, иногда вы действительно хотите Task<SomeType>
, но в большинстве случаев вам нужен экземпляр SomeType
то есть результат от задачи.
Из вашего кода:
async Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return await foo.DoAnotherThingAsync();
}
}
Человек, незнакомый с синтаксисом (я, например), может подумать, что этот метод должен возвращать Task<SomeResult>
, но так как он отмечен async
, это означает, что его фактический тип возвращаемого значения SomeResult
, Если вы просто используете return foo.DoAnotherThingAsync()
, вы бы возвращали задачу, которая не будет компилироваться. Правильный способ - вернуть результат задачи, поэтому return await
,
Еще одна причина, по которой вы можете захотеть
return await
:
await
синтаксис поддерживает автоматическое преобразование между типами и возвращаемого значения. Например, приведенный ниже код работает, даже если метод SubTask возвращает
Task<T>
но его вызывающий возвращается
ValueTask<T>
.
async Task<T> SubTask()
{
...
}
async ValueTask<T> DoSomething()
{
await UnimportantTask();
return await SubTask();
}
Если вы пропустите await на
DoSomething()
строка, вы получите ошибку компилятора CS0029:
Невозможно неявно преобразовать тип «System.Threading.Tasks.Task<BlaBla>» в «System.Threading.Tasks.ValueTask<BlaBla>».
Вы получите CS0030, если попытаетесь явно привести его к типу. Не беспокойтесь.
Это, кстати, .NET Framework. Я вполне могу предвидеть комментарий о том, что «это исправлено в гипотетической_версии .NET », я не проверял это. :)
Это предотвратит создание конечного автомата задачи, который будет создаваться «под капотом» во время компиляции. Но, по словам Дэвида, вам следует предпочесть async/await прямому возврату Task по следующим причинам:
- Асинхронные и синхронные исключения нормализуются так, чтобы всегда быть асинхронными.
- Код легче модифицировать (например, добавить использование).
- Диагностика асинхронных методов проще (отладка зависаний и т. д.).
- Вызванные исключения будут автоматически заключены в возвращаемую задачу вместо того, чтобы удивлять вызывающую сторону фактическим исключением.
- Асинхронные локальные переменные не будут вытекать из асинхронных методов. Если вы установите асинхронный локальный метод в неасинхронном методе, он «утечет» из этого вызова.
ПРИМЕЧАНИЕ. При использовании асинхронного конечного автомата вместо прямого возврата задачи существуют соображения производительности. Всегда быстрее вернуть задачу напрямую, поскольку она выполняет меньше работы, но в конечном итоге вы меняете поведение и потенциально теряете некоторые преимущества асинхронного конечного автомата.
Если вы действительно хотите избавиться от асинхронности, вы должны знать, что асинхронность вирусна :
Как только вы перейдете в асинхронный режим, все ваши вызывающие программы ДОЛЖНЫ быть асинхронными, поскольку усилия по обеспечению асинхронности ни к чему не приведут, если только весь стек вызовов не является асинхронным. Во многих случаях частичная асинхронность может быть хуже, чем полная синхронность. Поэтому лучше всего пойти ва-банк и сделать все асинхронным сразу.
Другая проблема с методом non-await заключается в том, что иногда вы не можете неявно привести тип возвращаемого значения, особенно сTask<IEnumerable<T>>
:
async Task<List<string>> GetListAsync(string foo) => new();
// This method works
async Task<IEnumerable<string>> GetMyList() => await GetListAsync("myFoo");
// This won't work
Task<IEnumerable<string>> GetMyListNoAsync() => GetListAsync("myFoo");
Ошибка:
Невозможно неявно преобразовать тип «System.Threading.Tasks.Task<System.Collections.Generic.List>» в «System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable>»