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

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

Ответ 1

От документы API (выделено мной):

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

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

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

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

Ответ 2

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

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

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

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

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

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

Может ли кто-нибудь объяснить, когда ValueTask не выполнит эту работу?

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

Ответ 3

ValueTask<T> не является подмножеством Task<T>, это надмножество.

ValueTask<T> - это дискриминированное объединение T и a Task<T>, что делает его без выделения для ReadAsync<T> для синхронного возврата значения T, которое оно имеет (в отличие от использования Task.FromResult<T>, которое необходимо выделить a Task<T>). ValueTask<T> является ожидаемым, поэтому наибольшее потребление экземпляров будет неотличимым от Task<T>.

ValueTask, являясь структурой, позволяет записывать методы async, которые не выделяют память, когда они запускаются синхронно, без ущерба для согласованности API. Представьте, что у вас есть интерфейс с методом возврата задачи. Каждый класс, реализующий этот интерфейс, должен возвращать Задачу, даже если они выполняются синхронно (надеюсь, используя Task.FromResult). Разумеется, на интерфейсе можно использовать два разных метода: синхронный и асинхронный, но для этого требуется две разные реализации, чтобы избежать "синхронизации по асинхронному" и "асинхронной синхронизации".

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

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

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

Это, по сути, то, что для сильной типизации.

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

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

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

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

Ответ 4

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

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

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

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

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

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

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

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