Чому можна використовувати Task <T> над ValueTask <T> у C #?


168

Станом на C # 7.0, методи асинхронізації можуть повернути ValueTask <T>. У поясненні сказано, що його слід використовувати, коли ми маємо кешований результат або моделюємо асинхронізацію за допомогою синхронного коду. Однак я все ще не розумію, у чому полягає проблема з використанням ValueTask завжди або насправді, чому async / await не був побудований із типом значення з самого початку. Коли ValueTask не зможе виконати роботу?


7
Я підозрюю, що це стосується переваг ValueTask<T>(з точки зору розподілів), які не реалізуються для операцій, які насправді є асинхронними (тому що в такому випадку ValueTask<T>все ще знадобиться розподіл купи). Є також питання про Task<T>те, що у бібліотеках є багато іншої підтримки.
Джон Скіт

3
@JonSkeet існуючі бібліотеки є проблемою, але це ставить питання, чи повинно було завдання бути ValueTask з самого початку? Переваги можуть не існувати при використанні його для реальних асинхронних матеріалів, але чи шкідливі вони?
Stilgar

8
Дивіться github.com/dotnet/corefx/isissue/4708#issuecomment-160658188 більше мудрості, ніж я міг би передати :)
Джон Скіт


3
@JoelMueller сюжет згущується :)
Stilgar

Відповіді:


245

З документів API (наголос додано):

Методи можуть повернути екземпляр цього типу значень, коли ймовірно, що результат їхніх операцій буде доступний синхронно і коли очікується, що метод буде викликаний настільки часто, що витрати на виділення нового Task<TResult>для кожного виклику будуть непомірними.

Існують компроміси щодо використання ValueTask<TResult>замість Task<TResult>. Наприклад, хоча а ValueTask<TResult>може допомогти уникнути розподілу у випадку, коли успішний результат доступний синхронно, він також містить два поля, тоді Task<TResult>як асистентний тип - це одне поле. Це означає, що виклик методу в кінцевому підсумку повертає два поля, варті даних, а не одне, що є більше даних для копіювання. Це також означає, що якщо метод, який повертає один із них, очікується в рамках asyncметоду, стан машини для цього asyncметоду буде більшим через необхідність зберігання структури, яка складається з двох полів, а не однієї посилання.

Крім того, для використання, крім споживання результату асинхронної операції через await, ValueTask<TResult>може призвести до більш складної моделі програмування, що, в свою чергу, може призвести до більшої кількості розподілів. Наприклад, розглянемо метод, який може повернути або Task<TResult>загальну результат із кешованим завданням, або a ValueTask<TResult>. Якщо споживач результату хоче використовувати його як такий Task<TResult>, як , наприклад, для використання у таких методах, як, Task.WhenAllі Task.WhenAny, ValueTask<TResult>спочатку потрібно перетворити його на Task<TResult>використання AsTask, що призводить до виділення, якого можна було б уникнути, якби кеш Task<TResult>використовувався на першому місці.

Таким чином, вибором за замовчуванням для будь-якого асинхронного методу повинен бути повернення a Taskабо Task<TResult>. Тільки якщо аналіз ефективності доведе, що це доцільно, слід ValueTask<TResult>використовувати його замість Task<TResult>.


7
@MattThomas: Це економить єдиний Taskрозподіл (який невеликий і дешевий в ці дні), але ціною збільшення наявного виділення абонента збільшується вдвічі і збільшується вдвічі більше, ніж повертається величина (що впливає на розподіл реєстру). Хоча це чіткий вибір сценарію зчитування з буферизованим режимом, його застосування за замовчуванням до всіх інтерфейсів - це не те, що я рекомендував би.
Стівен Клірі

1
Право, Taskабо ValueTaskможе використовуватися як синхронний тип повернення (з Task.FromResult). Але все ж є цінність (хе), ValueTaskякщо у вас є щось, що ви очікуєте бути синхронним. ReadByteAsyncбудучи класичним прикладом. Я вважаю, що ValueTask створено в основному для нових "каналів" (потоків байтів низького рівня), можливо, також використовуються в ядрі ASP.NET, де продуктивність дійсно важлива.
Стівен Клірі

1
О, я знаю, що хай, просто цікавилось, чи можна щось додати до цього конкретного коментаря;)
julealgon

2
Чи має це PR - перемикач балансу до віддають перевагу ValueTask? (ref: blog.marcgravell.com/2019/08/… )
stuartd

2
@stuartd: Наразі я б рекомендував використовувати Task<T>його за замовчуванням. Це лише тому, що більшість розробників не знайомі з обмеженнями навколо ValueTask<T>(зокрема, правило "тільки споживають один раз" і правило "без блокування"). Це ValueTask<T>означає , що якщо всі розробники у вашій команді хороші , я б рекомендував настанови на рівні команд віддати перевагу ValueTask<T>.
Стівен Клірі

104

Однак я все ще не розумію, у чому полягає проблема із використанням ValueTask завжди

Типи конструкцій не є безкоштовними. Копіювання структур, більших за розмір посилання, може бути повільнішим, ніж копіювання посилання. Зберігання структур, більших за еталон, займає більше пам'яті, ніж зберігання посилання. Структури розміром більше 64 біт не можуть бути зареєстровані, коли посилання може бути зареєстровано. Переваги нижчого тиску в зборі можуть не перевищувати витрат.

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

чому async / wait не було створено з типом значення з самого початку.

awaitбуло додано до C # довго після того, як Task<T>тип вже існував. Було б дещо викривленим вигадувати новий тип, коли він уже існував. І awaitпройшов безліч дизайнерських ітерацій, перш ніж зупинитися на тому, що був поставлений у 2012 році. Ідеальний - ворог блага; краще поставити рішення, яке добре працює з наявною інфраструктурою, а потім, якщо є попит користувача, надайте удосконалення пізніше.

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

Чи може хтось пояснити, коли ValueTask не виконав цю роботу?

Мета речі - поліпшення продуктивності. Це не робить цю роботу, якщо це не вимірює та значно покращує продуктивність. Немає гарантії, що так буде.


23

ValueTask<T>це не підмножина Task<T>, це суперсет .

ValueTask<T>є дискримінаційним об'єднанням T і a Task<T>, що робить його без розподілу для ReadAsync<T>синхронного повернення доступного йому значення T (на відміну від використання Task.FromResult<T>, якому потрібно виділити Task<T>екземпляр). ValueTask<T>є очікуваним, тому більшість споживання екземплярів буде невідрізним від типу "a" Task<T>.

ValueTask, будучи структурою, дозволяє записувати методи асинхронізації, які не виділяють пам'ять, коли вони працюють синхронно, не порушуючи послідовності API. Уявіть, що є інтерфейс із методом повернення задач. Кожен клас, що реалізує цей інтерфейс, повинен повернути завдання, навіть якщо вони трапляються синхронно (сподіваємось, використовуючи Task.FromResult). Звичайно, в інтерфейсі можуть бути два різних способу, синхронний та асинхронний, але для цього потрібно 2 різних реалізації, щоб уникнути "синхронізації через асинхронізацію" та "асинхронізації через синхронізацію".

Таким чином, ви можете написати один метод, який є або асинхронним, або синхронним, а не записати один ідентичний метод для кожного. Ви можете використовувати його де завгодно, Task<T>але це часто нічого не додає.

Що ж, це додає одне: воно додає передбачуване обіцянку абоненту, що метод насправді використовує додаткову функціональність, яку ValueTask<T>надає. Я особисто віддаю перевагу вибору параметрів та типів повернення, які максимально повідомляють абоненту. Не повертайтеся, IList<T>якщо перерахування не може забезпечити підрахунок; не повертайся, IEnumerable<T>якщо може. Ваші споживачі не повинні шукати будь-яку документацію, щоб знати, який із ваших методів можна розумно назвати синхронним, а який - не.

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

Це, по суті, те, для чого сильне введення тексту.

Якщо хтось із програмістів, які розробляють методи асинхронізації у вашому магазині, не можуть прийняти обґрунтованих рішень, може бути корисним призначити старшого наставника кожному з тих менш досвідчених програмістів і провести щотижневий огляд коду. Якщо вони здогадуються неправильно, поясніть, чому це слід робити інакше. Це накладні витрати для старших хлопців, але це призведе юніорів до швидкості набагато швидше, ніж просто кинути їх у глибокий кінець і дати їм якесь довільне правило, яке вони повинні виконувати.

Якщо хлопець, який написав метод, не знає, чи можна його називати синхронно, хто це на Землі ?!

Якщо у вас так багато недосвідчених програмістів, які пишуть методи асинхронізації, називають їх ті ж люди? Чи кваліфіковані вони самі, щоб зрозуміти, які з них можна безпечно називати асинхронією, чи вони почнуть застосовувати аналогічно довільне правило до того, як називають ці речі?

Проблема тут не у тому, що ти повертаєшся, а програмісти ставлять у ролі, до яких вони не готові. Це, мабуть, сталося з причини, тому я впевнений, що це неможливо виправдати. Опис його, безумовно, не є рішенням. Але пошук способу проникнути проблеми повз компілятор також не є рішенням.


4
Якщо я бачу метод, що повертається ValueTask<T>, я припускаю, що хлопець, який написав метод, це зробив, оскільки метод фактично використовує функціонал, який ValueTask<T>додає. Я не розумію, чому ви вважаєте, що бажано, щоб усі ваші методи мали однаковий тип повернення. Яка мета там?
15ee8f99-57ff-4f92-890c-b56153

2
Мета 1 - дозволити змінити реалізацію. Що робити, якщо я зараз не кешую результат, але пізніше додаю код кешування? Мета 2 - мати чітке керівництво для команди, де не всі члени розуміють, коли ValueTask корисний. Просто робіть це завжди, якщо в його використанні немає ніякої шкоди.
Стілгар

6
На мій погляд, це майже як повернення всіх ваших методів, objectтому що, можливо, колись ви захочете, щоб вони повернулися intзамість цього string.
15ee8f99-57ff-4f92-890c-b56153

3
Можливо, але якщо ви задасте питання "чому ви коли-небудь повертаєте рядок замість об'єкта", я можу легко відповісти на це, вказуючи на відсутність безпеки типу. Причини не використовувати ValueTask скрізь здаються набагато тонкішими.
Stilgar

3
@Stilgar Я б сказав одне: тонкі проблеми повинні хвилювати вас більше, ніж очевидні.
15ee8f99-57ff-4f92-890c-b56153

20

У .Net Core 2.1 є деякі зміни . Починаючи з ядра .net 2.1 ValueTask може представляти не тільки виконані синхронізовані дії, але й асинхронізацію. Крім того, ми отримуємо негенеричний ValueTaskтип.

Я залишу коментар Стівена Туба, який пов'язаний з вашим питанням:

Нам все ще потрібно формалізувати вказівки, але я думаю, що це буде щось подібне для публічної поверхні API API:

  • Завдання забезпечує найбільшу зручність використання.

  • ValueTask надає найбільше варіантів оптимізації продуктивності.

  • Якщо ви пишете інтерфейс / віртуальний метод, який інші переможуть, ValueTask - це правильний вибір за замовчуванням.

  • Якщо ви очікуєте, що API буде використовуватися на гарячих контурах, де значення мають розподіл, ValueTask - хороший вибір.

  • В іншому випадку, коли продуктивність не є критичною, задайте завдання Завдання, оскільки це забезпечує кращі гарантії та зручність використання.

З точки зору реалізації, багато повернених екземплярів ValueTask все ще будуть підтримуватися Завданням.

Функцію можна використовувати не тільки в ядрі .net 2.1. Ви зможете використовувати його з пакетом System.Threading.Tasks.Extensions .


1
Більше від Стівена сьогодні: blogs.msdn.microsoft.com/dotnet/2018/11/07/…
Майк Чіл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.