Зачем использовать Task <T> вместо ValueTask <T> в C #?

168

Начиная с C # 7.0 асинхронные методы могут возвращать ValueTask <T>. В объяснении говорится, что его следует использовать, когда мы имеем кешированный результат или имитируем асинхронность с помощью синхронного кода. Однако я до сих пор не понимаю, в чем проблема с использованием ValueTask всегда или на самом деле, почему async / await не был создан с типом значения с самого начала. Когда ValueTask не справится с этой задачей?

Stilgar
источник
7
Я подозреваю, что это связано с преимуществами ValueTask<T>(с точки зрения распределения) отсутствия материализации для операций, которые на самом деле являются асинхронными (потому что в этом случае ValueTask<T>все равно потребуется распределение кучи). Также Task<T>существует проблема с другой поддержкой в ​​библиотеках.
Джон Скит
3
@JonSkeet существующие библиотеки - это проблема, но возникает вопрос: должен ли был TaskTask с самого начала? Преимущества могут не существовать при использовании его для реальных асинхронных вещей, но вредно ли это?
Стилгар
8
См. Github.com/dotnet/corefx/issues/4708#issuecomment-160658188 для большей мудрости, чем я мог бы передать :)
Джон Скит
3
@JoelMueller сюжет утолщается :)
Stilgar

Ответы:

245

Из документации API (выделение добавлено):

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

Есть компромиссы с использованием ValueTask<TResult>вместо Task<TResult>. Например, хотя a ValueTask<TResult>может помочь избежать выделения в случае, когда успешный результат доступен синхронно, оно также содержит два поля, тогда как a Task<TResult>в качестве ссылочного типа является одним полем. Это означает, что вызов метода в итоге возвращает два поля данных вместо одного, то есть больше данных для копирования. Это также означает, что если в методе ожидается метод, который возвращает один из них, конечный asyncавтомат для этого asyncметода будет больше из-за необходимости хранить структуру, состоящую из двух полей вместо одной ссылки.

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

Таким образом, выбором по умолчанию для любого асинхронного метода должно быть возвращение Taskили Task<TResult>. Только если анализ производительности доказывает, что это целесообразно, следует ValueTask<TResult>использовать вместо Task<TResult>.

Стивен Клири
источник
7
@MattThomas: он сохраняет одно Taskвыделение (которое в наши дни является небольшим и дешевым), но за счет увеличения существующего выделения вызывающей стороны и удвоения размера возвращаемого значения (влияет на распределение регистров). Хотя это и является очевидным выбором для сценария с буферизованным чтением, применять его по умолчанию ко всем интерфейсам не рекомендуется.
Стивен Клири
1
Право, как Taskи ValueTaskможет быть использован в качестве синхронного типа возврата (с Task.FromResult). Но все же есть смысл (хех), ValueTaskесли у вас есть что-то, что вы ожидаете быть синхронным. ReadByteAsyncбыть классическим примером. Я считаю, что он ValueTask был создан в основном для новых «каналов» (низкоуровневых байтовых потоков), возможно, также используемых в ядре ASP.NET, где производительность действительно имеет значение.
Стивен Клири
1
О, я знаю, что лол, просто интересно, есть ли у вас что-то, чтобы добавить к этому конкретному комментарию;)
Jullealgon
2
Имеет ли это PR - переключатель баланса к предпочитающим ValueTask? (ссылка: blog.marcgravell.com/2019/08/... )
stuartd
2
@stuartd: На данный момент я бы по-прежнему рекомендовал использовать Task<T>по умолчанию. Это только потому, что большинство разработчиков не знакомы с ограничениями ValueTask<T>(в частности, с правилом «только один раз» и «без блокировки»). Тем не менее, если все разработчики в вашей команде хороши ValueTask<T>, то я бы порекомендовал руководящие указания на уровне команды ValueTask<T>.
Стивен Клири
104

Однако я до сих пор не понимаю, в чем проблема с использованием ValueTask всегда

Типы структур не являются бесплатными. Копирование структур, размер которых превышает размер ссылки, может быть медленнее, чем копирование ссылки. Хранение структур, превышающих ссылку, занимает больше памяти, чем сохранение ссылки. Структуры, размер которых превышает 64 бита, могут не быть зарегистрированы, когда ссылка может быть зарегистрирована. Преимущества более низкого давления сбора не могут превышать затраты.

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

почему async / await не был создан с типом значения с самого начала.

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

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

Может кто-нибудь объяснить, когда 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. Каждый класс, реализующий этот интерфейс, должен возвращать Задачу, даже если они выполняются синхронно (возможно, с использованием Task.FromResult). Конечно, вы можете иметь 2 разных метода на интерфейсе: синхронный и асинхронный, но для этого требуется 2 разных реализации, чтобы избежать «синхронизации по асинхронности» и «асинхронизации по синхронизации».

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

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

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

По сути, это то, для чего нужна строгая типизация.

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

Если парень, который написал метод, не знает, может ли он быть вызван синхронно, то кто на Земле знает ?!

Если у вас так много неопытных программистов, пишущих асинхронные методы, эти же люди их тоже называют? Могут ли они сами определить, какие из них безопасно назвать асинхронными, или же они начнут применять подобное произвольное правило к тому, как они называют эти вещи?

Проблема здесь не в ваших типах возвращаемых данных, а в том, что программистов ставят в роли, к которым они не готовы. Это должно было произойти по какой-то причине, поэтому я уверен, что это не может быть тривиально исправить. Описание этого, конечно, не является решением. Но поиск способа обойти проблему без компилятора также не является решением.

15ee8f99-57ff-4f92-890c-b56153
источник
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 везде кажутся более тонкими.
Стилгар
3
@ Стилгар Я хотел бы сказать одно: тонкие проблемы должны волновать вас больше, чем очевидные.
15ee8f99-57ff-4f92-890c-b56153
20

В .Net Core 2.1 есть некоторые изменения . Начиная с .net core 2.1, ValueTask может представлять не только синхронно выполненные действия, но и асинхронное выполнение. Кроме того, мы получаем неуниверсальный ValueTaskтип.

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

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

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

  • ValueTask предоставляет больше возможностей для оптимизации производительности.

  • Если вы пишете интерфейс / виртуальный метод, который другие будут переопределять, ValueTask будет правильным выбором по умолчанию.

  • Если вы ожидаете, что API будет использоваться на горячих путях, где распределение будет иметь значение, ValueTask будет хорошим выбором.

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

С точки зрения реализации, многие из возвращенных экземпляров ValueTask будут по-прежнему поддерживаться Task.

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

unsafePtr
источник
1
Больше от Стивена сегодня: blogs.msdn.microsoft.com/dotnet/2018/11/07/…
Майк Чил