» » Почему, зачем и когда нужно использовать ValueTask

 

Почему, зачем и когда нужно использовать ValueTask

Автор: admin от 4-07-2019, 19:45, посмотрело: 112

Этот перевод появился благодаря хорошему комментарию 0x1000000.

Почему, зачем и когда нужно использовать ValueTask



В .NET Framework 4 появилось пространство System.Threading.Tasks, а с ним и класс Task. Этот тип и порождённый от него Task долго дожидались, пока их признают стандартами в .NET в роли ключевых аспектов модели асинхронного программирования, которая была представлена в C# 5 с его операторами async/await. В этой статье я расскажу о новых типах ValueTask/ValueTask, разработанных для улучшения производительности асинхронных методов в случаях, когда издержки на выделение памяти нужно принимать во внимание.

https://github.com/dotnet/corefx/issues/32664 в репозитории dotnet/corefx.



Паттерны применения ValueTasks



На первый взгляд область применения ValueTask и ValueTask намного более ограничена, чем Task и Task. Это хорошо, и даже ожидаемо, так как основной способ их применения – просто использование с оператором await.



Однако, так как они могут оборачивать объекты, которые повторно используются, существуют значительные ограничения по их применению в сравнении с Task и Task, если отклониться от обычного способа простого await. В общих случаях, следующие операции никогда не должны выполняться с ValueTask/ValueTask:




  • Повторное ожидание ValueTask/ValueTask Объект результата может уже быть утилизирован и использоваться в другой операции. Напротив, Task/Task никогда не переходит из завершённого состояния в незавершённое, поэтому вы можете повторно ожидать его столько раз, сколько потребуется, и получать один и тот же результат каждый раз.

  • Параллельное ожидание ValueTask/ValueTask Объект результата ожидает обработки только одним callbackом от одного потребителя в один момент времени, и попытка его ожидания из разных потоков одновременно может легко привести к гонкам и трудноуловимым ошибкам программы. Кроме того, это также более специфичный случай предыдущей недопустимой операции «повторное ожидание». В сравнении с этим, Task/Task обеспечивает любое количество параллельных awaitов.

  • Использование .GetAwaiter().GetResult(), когда операция ещё не завершилась Реализация IValueTaskSource/IValueTaskSource не нуждается в поддержке блокировки до окончания операции, и, скорее всего, не сделает этого, так что такая операция определённо приведёт к гонкам и вероятно будет выполнена не так, как ожидает вызывающий метод. Task/Task блокирует вызывающий поток пока задача не будет выполнена.



Если получили ValueTask или ValueTask, но необходимо выполнить одну из этих трёх операций, вы можете использовать .AsTask(), получить Task/Task и после этого работать с полученным объектом. После этого вы больше не сможете использовать тот ValueTask/ValueTask.



Короче говоря, правило таково: при применении ValueTask/ValueTask вы должны или await его непосредственно (возможно с .ConfigureAwait(false)) или вызвать AsTask() и больше его не использовать:



// Дан такой метод, возвращающий ValueTask<int>
public ValueTask<int> SomeValueTaskReturningMethodAsync();
...
// GOOD
int result = await SomeValueTaskReturningMethodAsync();

// GOOD
int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false);

// GOOD
Task<int> t = SomeValueTaskReturningMethodAsync().AsTask();

// WARNING
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
// сохранение экземпляра локально делает это потенциально небезопасным,
// но может быть безвредным

// BAD: await несколько раз
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
int result = await vt;
int result2 = await vt;

// BAD: await параллельно (и по определению несколько раз)
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
Task.Run(async () => await vt);
Task.Run(async () => await vt);

// BAD: использование GetAwaiter().GetResult(), если неизвестно о завершении
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
int result = vt.GetAwaiter().GetResult();


Есть ещё один продвинутый паттерн, который программисты могут применять, надеюсь, только после аккуратного измерения и получения существенных преимуществ. Классы ValueTask/ValueTask имеют несколько свойств, которые сообщают о текущем состоянии операции, например, свойство IsCompleted возвращает true, если операция выполнилась (то есть, больше не выполняется и завершилась успешно или не успешно), а свойство IsCompletedSuccessfully возвращает true, только если завершилась успешно (при ожидании и получении результата не выбросило исключения). Для самых напряжённых потоков выполнения, там, где разработчик хочет избежать затрат, которые появляются при асинхронном режиме, эти свойства могут быть проверены перед операцией, которая фактически разрушит объект ValueTask/ValueTask, например await, .AsTask(). Например, в реализации SocketsHttpHandler в .NET Core 2.1 код выполняет чтение из соединения и получает ValueTask. Если эта операция выполнится синхронно, нам не стоит беспокоиться о досрочном прерывании операции. Но если она выполняется асинхронно, мы должны подцепить обработку прерывание, чтобы запрос прерывания разорвал соединение. Так как это очень напряжённый участок кода, если профилирование покажет необходимость в следующем небольшом изменении, его можно структурировать так:



int bytesRead;
{
    ValueTask<int> readTask = _connection.ReadAsync(buffer);
    if (readTask.IsCompletedSuccessfully)
    {
        bytesRead = readTask.Result;
    }
    else
    {
        using (_connection.RegisterCancellation())
        {
            bytesRead = await readTask;
        }
    }
}


Должен ли каждый новый метод асинхронного API возвращать ValueTask/ValueTask?



Если ответить кратко: нет, по умолчанию стоит по-прежнему выбирать Task/Task.

Как подчёркивать выше, Task и Task использовать правильно легче, чем ValueTask и ValueTask, и до тех пор, пока требования производительность не перевешивают требования практичности, Task и Task предпочтительны. Кроме того, есть небольшие затраты, связанные с возвращением ValueTask вместо Task, то есть, микробенчмарки показывают, что await Task выполняется быстрее, чем await ValueTask. Так что, если вы используете кеширование задач, например, ваш метод возвращает Task или Task, для производительности стоит остаться с Task или Task. Объекты ValueTask/ValueTask занимают в памяти несколько слов, поэтому, когда они ожидаются, и поля для них резервируются в вызывающей async метод машине состояний, они будут занимать в ней больше памяти.

И всё-таки ValueTask/ValueTask будут отличным выбором когда: а) вы ожидаете, что вызывать ваш метод будут только с await, б) затраты на выделение памяти критичны для вашей разработки, в) синхронное выполнение будет происходить часто, или вы сможете эффективно повторно использовать объекты при асинхронном выполнении. При добавлении абстрактных, виртуальных или интерфейсных методов вы должны рассмотреть будут ли так же выполняться эти условия при перегрузке/реализации этих методов.



Что дальше с ValueTask и ValueTask?



Для системных библиотек .NET мы будем продолжать работать с методами, возвращающими Task/Task, но методы, возвращающие ValueTask/ValueTask, так же будут добавляться там, где это необходимо. Один ключевой пример таких методов – это новый IAsyncEnumerator, который должен появиться в .NET Core 3.0. IEnumerator имеет метод MoveNext, который возвращает bool, и его асинхронный аналог IAsyncEnumerator предоставляет метод MoveNextAsync. Когда мы начали его разрабатывать, думали, что он должен возвращать Task, который может быть очень эффективен кешированными задачами при частом завершении в синхронном режиме. Однако, в виду того, как разнообразны могут быть асинхронные перечислимые типы, как разнообразны могут быть реализации этого интерфейса (и для некоторых из них очень критичны проблемы производительности и выделения памяти), и что основным способом их использования будет await в конструкции foreach, мы остановились на варианте с ValueTask. Это позволит операциям с синхронным завершением выполняться быстро, а для асинхронного завершения будет доступна возможность экономии памяти. И на самом деле компилятор C# использует эти преимущества, когда создаёт асинхронные итераторы, реализуя их без выделения памяти, если это возможно.



Источник: Хабр / Интересные публикации

Категория: Программирование

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *