Яка мета "очікування повернення" в C #?


251

Чи є якийсь - або сценарій , де метод запису , як це:

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такою кількістю місць, я думаю, що, можливо, я щось пропустив. Але наскільки я розумію, не використовувати ключові слова асинхрон / очікувати в цьому випадку та безпосередньо повернути завдання було б функціонально рівнозначним. Навіщо додавати додаткові накладні витрати додаткового awaitшару?


2
Я думаю, що єдина причина, чому ви це бачите, - це те, що люди навчаються шляхом наслідування і, як правило, (якщо вони не потребують) вони використовують найпростіші рішення, які вони можуть знайти. Тож люди бачать цей код, використовують цей код, вони бачать, що він працює, і відтепер для них це правильний спосіб зробити це ... У цьому випадку чекати не має сенсу
Фабіо Марколіні

7
Принаймні одна важлива відмінність: поширення виключень .
носраціо

1
Я теж не розумію цього, не можу зрозуміти всю цю концепцію взагалі, не має сенсу. З того, що я дізнався, якщо метод має тип повернення, ОБОВ'ЯЗКОВО мати ключове слово повернення, чи не це правила мови 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


4
Для повноти, в першому випадку вам слід повернутисяfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord

7
@ghord Це не спрацювало, Dispose()повертається void. Вам знадобиться щось на кшталт return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Але я не знаю, навіщо це робити, коли ти можеш використовувати другий варіант.
svick

1
@svick Ви праві, це повинно бути більше за темою { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Випадок використання досить простий: якщо ви перебуваєте на .NET 4.0 (як і більшість), ви все одно можете написати асинхронний код таким чином, який буде добре працювати з 4,5 додатків.
ghord

2
@ghord Якщо ви перебуваєте на .Net 4.0 і хочете написати асинхронний код, ви, ймовірно, повинні використовувати Microsoft.Bcl.Async . А ваш код розпоряджається Fooлише після завершення повернення Task, що мені не подобається, тому що він зайво вносить паралельність.
svick

1
@svick Ваш код чекає, поки завдання теж не буде закінчено. Крім того, Microsoft.Bcl.Async для мене непридатний через залежність від KB2468871 та конфліктів при використанні коду .NET 4.0 async з належним 4.5 кодом асинхронізації.
ghord

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і тут
зайве

23
@MattSmith Це не збирається. Якщо ви хочете використовувати awaitв першому рядку, ви повинні використовувати його і в другому.
svick

2
@cateyes Я не впевнений, що означає "накладні витрати, паралельні їм", але asyncверсія використовуватиме менше ресурсів (потоків), ніж ваша синхронна версія.
svick

4
@TomLint Дійсно не компілюється. Якщо припустити, що тип повернення SecondAwaitдорівнює "рядок", повідомлення про помилку: "CS4016: Оскільки це метод асинхронізації, вираз повернення повинен мати тип" string ", а не" Task <string> "".
svick

23

Єдина причина, яку ви хочете це зробити, - це якщо awaitв попередньому коді є якийсь інший , або якщо ви якимось чином маніпулюєте результатом, перш ніж повернути його. Інший спосіб, як це може статися, - це через try/catchте, що змінює поводження з винятками. Якщо ви нічого з цього не зробите, то ви праві, немає ніяких причин додавати накладні витрати на створення методу async.


4
Як і у відповідь Стівена, я не розумію, для чого це було return awaitб потрібно (замість того, щоб просто повертати завдання виклику дитини), навіть якщо в попередньому коді чекають інші . Чи можете ви надати пояснення?
TX_

10
@TX_ Якщо ви хочете видалити, asyncяк би ви очікували першого завдання? Потрібно позначити метод так, asyncніби ви хочете використовувати будь-які очікування. Якщо метод позначений як asyncі у вас є більш awaitранній код, тоді вам потрібно виконати awaitдругу операцію асинхронізації, щоб він був належного типу. Якщо ви тільки що видалили, awaitто він не складеться, оскільки значення, що повертається, не було б належного типу. Оскільки метод - asyncрезультат, завжди обгортається завданням.
Сервіс

11
@Noseratio Спробуйте два. Перші складають. Другий - ні. Повідомлення про помилку підкаже вам проблему. Ви не повернете належний тип. Коли в asyncметоді ви не повертаєте завдання, ви повертаєте результат завдання, який потім буде завернуто.
Сервіс

3
@ Серві, звичайно - ти маєш рацію. В останньому випадку ми повертаємося Task<Type>явно, тоді як asyncдиктуємо повернення Type(в який перетворився б сам компілятор Task<Type>).
nosratio

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

Здійснення інакше простого методу «обертання» async створює машину стану асинхронізації в пам’яті, тоді як не-асинхронний - ні. Хоча це часто може вказувати людям на використання не-асинхронної версії, оскільки вона є більш ефективною (що правда), це також означає, що у випадку зависання у вас немає доказів того, що цей метод бере участь у "стеці повернення / продовження" що іноді ускладнює розуміння повіси.

Тож так, коли перф не є критичним (а зазвичай це не так), я кину асинхронізу на всі ці способи громозвіння, щоб у мене з'явилася машина стану асинхронізації, яка допоможе мені діагностувати зависання пізніше, а також, щоб переконатися, що якщо ці Способи громіння колись розвиваються з часом, вони обов'язково повернуть несправні завдання замість кидка.


6

Якщо ви не будете використовувати return, чекайте, ви можете зруйнувати слід стека під час налагодження або коли він надрукується в журналах за винятками.

Після повернення завдання метод виконав своє призначення і він знаходиться поза стеком викликів. Коли ви використовуєтеreturn await ви залишаєте його в стеку дзвінків.

Наприклад:

Стек викликів при використанні в режимі await: Очікування завдання від B => B очікування завдання від C

Стек викликів, коли не використовується функція await: Очікування завдання від C, яке B повернуло.


2

Це також бентежить мене, і я відчуваю, що попередні відповіді не помітили вашого актуального питання:

Навіщо використовувати конструкцію return очікувати, коли ви можете безпосередньо повернути завдання із внутрішнього виклику DoAbodyThingAsync ()?

Ну, іноді ви дійсно хочете 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.


1
"фактичний тип повернення". Так? async / wait не змінює типи повернення. У вашому прикладі var task = DoSomethingAsync();ви дасте вам завдання, а неT
взуття

2
@Shoe Я не впевнений, що я добре зрозумів async/awaitріч. Наскільки я розумію Task task = DoSomethingAsync(), поки Something something = await DoSomethingAsync()обидва працюють. Перший дає вам належне завдання, а другий, за awaitключовим словом, дає результат від завдання після його виконання. Я міг, наприклад, мати Task task = DoSomethingAsync(); Something something = await task;.
heltonbiker
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.