вторник, 7 января 2025 г.

Да кто такой этот ваш ValueTask?!

   Всем привет! Ну вот и подходят к концу новогодние праздники. Решил потихоньку приподнять лицо из оливье и попробовать, попытаться объяснить описать возможности класса ValueTask (оставляю ссылку на официальную документацию).

    Но давайте сначала разберёмся с Task в C#
  
    В .NET Framework 4 появилось пространство System.Threading.Tasks, а с ним и класс Task. Этот тип и порождённый от него Task<TResult> долго дожидались, пока их признают стандартами в .NET в роли ключевых аспектов модели асинхронного программирования, которая была представлена в C# 5 с его операторами async/await. 
    Task является очень гибким классом и это даёт ряд преимуществ. Например, можно выполнить await несколько раз для любого количества потребителей одновременно. А можно положить его в коллекцию (dictionary) для повторных await в будущем, чтобы использовать его как кеш результатов асинхронных вызовов. Также можно заблокировать выполнение, ожидая завершения Task, если такое понадобится. И также можно написать и применить разнообразные операции над объектами Task (иногда их называют «комбинаторами»), например, «когда любая» (“when any”) для асинхронного ожидания первого завершения из нескольких Task.
     Но эта гибкость становится лишней в наиболее часто встречающемся случае: просто вызвать асинхронную операцию и дождаться выполнения задачи:

TResult result = await SomeOperationAsync();
UseResult(result);

     Здесь не понадобится многократного ожидания выполнения участка кода. Нет необходимости обеспечить конкуррентность ожиданий и выполнения синхронной блокировки или написания комбинаторов. Мы просто ждём выполнения promise асинхронной операции. В конце концов, это так, как мы пишем синхронный код (например, TResult result = SomeOperation();), и это обычным образом переводится на язык async/await.
     Более того, у Task есть потенциальная слабая сторона, особенно когда создаётся большое количество его экземпляров, а большая пропускная способность и производительность являются ключевыми требованиями – Task является классом. Это означает, что любая операция, которой понадобился Task, вынуждена создавать и размещать объект в хипе, а чем больше объектов создаётся, тем больше работы для сборщика мусора (GC), и на эту работу расходуются ресурсы, которые мы могли бы потратить на что-то более полезное.

    А при чём тут ValueTask?
    Всё это потребовало реализации в .NET Core 2.0 нового типа, который доступен в предыдущих версиях .NET в пакете NuGet System.Threading.Tasks.Extensions: ValueTask<TResult>. Он был создан в .NET Core 2.0 как структура, способная обернуть как TResult, так и Task<TResult>. Это означает, что её можно возвращать из async метода, и, если этот метод выполнится синхронно и успешно, никакого объекта в куче размещать не надо: вы можете просто инициализировать эту структуру ValueTask<TResult> значением TResult и вернуть. Только в случае асинхронного выполнения объект Task<TResult> будет размещён, а ValueTask<TResult> обернёт его (чтобы минимизировать размер структуры и оптимизировать случай успешного исполнения, async метод, который завершается с неподдерживаемым исключением, также будет размещать Task<TResult>, так что ValueTask<TResult> так же просто обернёт Task<TResult>, а не будет таскать с собой дополнительное поле для хранения Exception).

Если подытожить, то ValueTask — это структура в .NET, которая используется для представления асинхронной операции. Она является альтернативой Task, но предназначена для более эффективного управления ресурсами в случаях, когда асинхронная операция может завершаться очень быстро, возможно даже синхронно.

Итого, можно следующим образом констатировать особенности использования ValueTask:

1️⃣Повторное ожидание запрещено: вызывать await несколько раз для одного и того же ValueTask нельзя, так как это может привести к неожиданным результатам.
❌ Так делать нельзя:
ValueTask<int> task = GetValueAsync();
int value1 = await task;
int value2 = await task; // Тут будет ошибка

2️⃣Конвертация в Task: если требуется передать ValueTask в API, которое ожидает Task, можно вызвать метод .AsTask():
Task<int> task = GetValueAsync().AsTask();

3️⃣Когда использовать ValueTask:
✅Операция часто завершается синхронно.
✅Создание объекта Task может быть слишком накладным.
✅Вы пишете библиотеку с высокой производительностью.

4️⃣Когда НЕ использовать ValueTask:
❌Операция всегда асинхронна.
❌Производительность не является критически важной.
❌Приложение не оптимизировано под работу с ValueTask.

Надеюсь, уважаемый читатель, что данная статья полностью объяснила разницу между 2-я классами и теперь не будет сложности в их использовании на практике.

Источники:

Комментариев нет:

Отправить комментарий