Какова цель «возврата ждать» в C #?

252

Есть ли какой - либо сценарий , где метод записи , как это:

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слой?

TX_
источник
2
Я думаю, что единственная причина, почему вы видите это, состоит в том, что люди учатся подражанием и обычно (если они не нуждаются) они используют самое простое решение, которое они могут найти. Таким образом, люди видят этот код, используют этот код, они видят, как он работает, и теперь для них это правильный способ сделать это ... Бесполезно ждать в таком случае
Фабио Марколини
7
Есть по крайней мере одно важное отличие: распространение исключений .
нос
1
Я тоже не понимаю, не могу понять всю эту концепцию, не имеет никакого смысла. Из того, что я узнал, если у метода есть тип возвращаемого значения, он ДОЛЖЕН иметь ключевое слово return, разве это не правила языка C #?
monstro
@monstro вопрос ОП есть оператор возврата хотя?
Дэвид Клемпфнер

Ответы:

189

Есть один хитрый случай, когда 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()FooDoAnotherThingAsync()Foo

svick
источник
4
Для полноты, в первом случае вы должны вернутьсяfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord
7
@ghord Это не сработает, Dispose()возвращается void. Вам нужно что-то вроде return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Но я не знаю, зачем вам это делать, когда вы можете использовать второй вариант.
Свик
1
@svick Вы правы, это должно быть больше по линии { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Вариант использования довольно прост: если вы находитесь на .NET 4.0 (как и большинство), вы все равно можете написать асинхронный код таким образом, который будет прекрасно работать из приложений 4.5.
Горд
2
@ghord Если вы используете .Net 4.0 и хотите писать асинхронный код, вам, вероятно, следует использовать Microsoft.Bcl.Async . И ваш код удаляется Fooтолько после того, как возвращенные Taskзавершены, что мне не нравится, потому что он излишне вводит параллелизм.
svick
1
@svick Ваш код ждет, пока задача тоже не будет завершена. Кроме того, Microsoft.Bcl.Async для меня непригоден из-за зависимости от KB2468871 и конфликтов при использовании асинхронной кодовой базы .NET 4.0 с надлежащим асинхронным кодом 4.5.
Горд
93

Если вам не нужно async(то есть вы можете вернуть Taskнапрямую), то не используйте async.

В некоторых ситуациях return awaitэто полезно, например, если вам нужно выполнить две асинхронные операции:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

Подробнее о asyncпроизводительности читайте в статье Стивена Тауба и видео MSDN на эту тему.

Обновление: я написал сообщение в блоге, которое входит в гораздо более подробную информацию.

Стивен Клири
источник
13
Не могли бы вы объяснить, почему awaitэто полезно во втором случае? Почему бы не сделать return SecondAwait(intermediate);?
Мэтт Смит
2
У меня такой же вопрос, как и у Мэтта, разве return SecondAwait(intermediate);в этом случае я не достигну цели? Я думаю, что return awaitи здесь избыточно ...
TX_
23
@MattSmith Это не скомпилируется. Если вы хотите использовать awaitв первой строке, вы должны использовать его и во второй.
svick
2
@cateyes Я не уверен, что означает «накладные расходы, связанные с параллелизмом», но asyncверсия будет использовать меньше ресурсов (потоков), чем ваша синхронная версия.
свик
4
@TomLint Это действительно не компилируется. Предполагая, что возвращаемым типом SecondAwaitявляется `строка ', появляется сообщение об ошибке:" CS4016: поскольку это асинхронный метод, возвращаемое выражение должно иметь тип' строка ', а не "Task <string>'".
svick
23

Единственная причина, по которой вы захотите это сделать, - это то, что awaitв более раннем коде есть какие-то другие , или если вы каким-то образом манипулируете результатом перед его возвратом. Другим способом, которым это может происходить, является try/catchизменение, которое обрабатывает исключения. Если вы ничего не делаете, тогда вы правы, нет никаких причин добавлять накладные расходы на создание метода async.

Servy
источник
4
Как и в случае ответа Стивена, я не понимаю, зачем это return awaitнужно (вместо того, чтобы просто возвращать задачу дочернего вызова), даже если в более раннем коде есть еще какие-то ожидания . Не могли бы вы дать объяснение?
TX_
10
@TX_ Если вы хотите удалить, asyncто как вы будете ждать первого задания? Вам нужно пометить метод так, как asyncбудто вы хотите использовать любые ожидающие. Если метод помечен как, asyncи у вас есть более awaitранний код, то вам нужно выполнить awaitвторую асинхронную операцию, чтобы он имел правильный тип. Если вы только что удалили, awaitон не скомпилируется, так как возвращаемое значение не будет правильного типа. Поскольку метод является asyncрезультатом, он всегда заключен в задачу.
Servy
11
@Noseratio Попробуйте два. Первые компиляции. Второй нет. Сообщение об ошибке сообщит вам о проблеме. Вы не будете возвращать правильный тип. Когда в asyncметоде вы не возвращаете задачу, вы возвращаете результат задачи, который затем будет перенесен.
Servy
3
@ Служу, конечно - ты прав. В последнем случае мы возвращаемся Task<Type>явно, в то время как asyncдиктует возврат Type(во что превратился бы сам компилятор Task<Type>).
нос
3
@Itsik Ну конечно, asyncэто просто синтаксический сахар для явного продолжения. Вам не нужно async ничего делать, но при выполнении любой нетривиальной асинхронной операции работать с ней значительно проще. Например, код, который вы предоставили, на самом деле не распространяет ошибки так, как вы этого хотите, и делать это правильно даже в более сложных ситуациях становится намного сложнее. В то время как вам никогда не нужно async , ситуации, которые я описываю, это то, где это добавляет ценность для его использования.
Обслуживание
17

Еще один случай, вам может потребоваться дождаться результата:

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>и все готово.

Эндрю Арнотт
источник
1
Согласитесь, это действительно раздражает - я считаю, что основной причиной является то, что Task<>является инвариантным.
StuartLC
7

Создание в противном случае простого метода «thunk» async создает в памяти асинхронный конечный автомат, а не асинхронный - нет. Хотя это часто может указывать людям на использование неасинхронной версии, поскольку она более эффективна (что верно), это также означает, что в случае зависания у вас нет никаких доказательств того, что этот метод задействован в «стеке возврата / продолжения» что иногда затрудняет понимание зависания.

Так что да, когда perf не критичен (и обычно это не так), я включу асинхронность во всех этих методах thunk, чтобы у меня был асинхронный конечный автомат, чтобы помочь мне диагностировать зависания позже, а также чтобы гарантировать, что если эти Методы thunk постоянно развиваются, и они будут возвращать ошибочные задачи вместо броска.

Эндрю Арнотт
источник
6

Если вы не используете return await, вы можете испортить трассировку стека во время отладки или когда она печатается в журналах исключений.

Когда вы возвращаете задачу, метод выполнил свою задачу, и он вышел из стека вызовов. Когда вы используетеreturn await вы оставляете его в стеке вызовов.

Например:

Стек вызовов при использовании await: A ожидает задачи от B => B ожидает задачи от C

Стек вызовов, когда не используется await: A ожидает задачу от C, которую B вернул.

haimb
источник
2

Это также смущает меня, и я чувствую, что предыдущие ответы упустили из виду ваш актуальный вопрос:

Зачем использовать конструкцию return await, если вы можете напрямую вернуть Task из внутреннего вызова DoAnotherThingAsync ()?

Ну, иногда вы действительно хотите Task<SomeType>, но в большинстве случаев вам нужен экземплярSomeType , то есть результат задачи.

Из вашего кода:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Человек, незнакомый с синтаксисом (например, я), может подумать, что этот метод должен возвращать a Task<SomeResult>, но, поскольку он помечен async, это означает, что его фактический тип возврата равен SomeResult. Если вы просто используете return foo.DoAnotherThingAsync(), вы будете возвращать задачу, которая не будет компилироваться. Правильный способ - вернуть результат задачи, поэтому return await.

heltonbiker
источник
1
"фактический тип возврата". А? async / await не меняет типы возвращаемых данных. В вашем примере var task = DoSomethingAsync();вам дадут задание, а неT
Чистка
2
@ Я не уверен, что хорошо понял эту async/awaitвещь. Насколько я понимаю Task task = DoSomethingAsync(), пока Something something = await DoSomethingAsync()оба работают. Первый дает вам задачу, а второй, благодаря awaitключевому слову, дает вам результат задачи после ее завершения. Я мог бы, например, иметь Task task = DoSomethingAsync(); Something something = await task;.
Heltonbiker