Вам нужно дождаться ValueTask?

Название говорит само за себя:

В спецификациях прямо указано, что вы никогда не должны ожидать ValueTask более одного раза, поскольку, в частности, в действительно асинхронном случае, резервный конечный автомат может быть объединен в пул и перезапущен для других ValueTask после его ожидания.

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

async Task IsThisLegal()
{
  var _ = SomeValueTask();
  await Task.Delay(10);
  // The value task would have completed by now, but we're not awaiting it. Is that ok?
}

async ValueTask<bool> SomeValueTask()
{
  await Task.Delay(TimeSpan.FromSeconds(1));
  return true;
}

Чего вы пытаетесь достичь? Это звонок «пожарь и забудь»? Собираетесь ли вы позже вызвать .Result по задаче значения?

Ortiga 04.07.2024 18:06
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
92
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Как вы отметили, метод возврата ValueTask «выстрелил и забыл» явно не описан в онлайн-документации :

Экземпляр ValueTask можно ожидать только один раз, и потребители не может прочитать Result, пока экземпляр не завершится.

Следующие операции никогда не следует выполнять на Экземпляр ValueTask:

  • Ожидание экземпляра несколько раз.
  • Вызов AsTask несколько раз.
  • Использование .Result или .GetAwaiter().GetResult(), когда операция еще не завершена, или использование их несколько раз.
  • Использование более чем одного из этих методов для использования экземпляра.

Если вы сделаете что-либо из вышеперечисленного, результаты будут неопределенными.

Все эти ограничения связаны с возможностью использовать экземпляры IValueTaskSource для создания ValueTask. Это интерфейс, который каждый может очень легко реализовать, используя ManualResetValueTaskSourceCore так, как предложил Стивен Тауб:

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

Например:

public class SimpleValueTaskSourceImplementer<T> : IValueTaskSource<T> {
    public ManualResetValueTaskSourceCore<T> manualResetTask = new(); // public for debugging purposes
    
    public T GetResult(short token) {
        return manualResetTask.GetResult(token);
    }
    public ValueTaskSourceStatus GetStatus(short token) {
        return manualResetTask.GetStatus(token);
    }
    public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) {
        manualResetTask.OnCompleted(continuation, state, token, flags);
    }
}

Тогда мы сможем использовать его в этом демонстрационном классе, возвращаясь ValueTask<int>

public class AsyncClass {

    SimpleValueTaskSourceImplementer<int> valueTaskSource = new();
    int m_counter = 42;
    
    public ValueTask<int> GetNextValueAsync() {
        valueTaskSource.manualResetTask.Reset(); 
        short version = valueTaskSource.manualResetTask.Version;

        Interlocked.Increment(ref m_counter);
        var captureCounter = m_counter;
        Task.Run(() => {
            Thread.Sleep(1000);
            valueTaskSource.manualResetTask.SetResult(captureCounter);
        });

        return new ValueTask<int>(valueTaskSource, version);
    }
}

Давайте проверим это с помощью await:

async Task Main() {
    var asyncClass = new AsyncClass();
    var result1 = await asyncClass.GetNextValueAsync();
    Console.WriteLine(result1); // 43
    var result2 = await asyncClass.GetNextValueAsync();
    Console.WriteLine(result2); // 44
}

Оно работает.

Но что, если мы сделаем .Result и .GetAwaiter().GetResult(), что согласно документации будет неопределённым поведением?

var asyncClass = new AsyncClass();
try {
    var result1 = asyncClass.GetNextValueAsync().Result;
} catch (InvalidOperationException ex ) {
    Console.WriteLine(ex.Message); // Operation is not valid due to the current state of the object.
}

try {
    var result1 = asyncClass.GetNextValueAsync().GetAwaiter().GetResult();
} catch (InvalidOperationException ex) {
    Console.WriteLine(ex.Message); // Operation is not valid due to the current state of the object.
}

Мы получаем исключения, но в любом случае это должно быть неопределенное поведение.

Теперь, если разработчик SimpleValueTaskSourceImplementer<T> и AsyncClass будет достаточно доволен и отправит это, ваш случай может стать проблематичным:

var asyncClass = new AsyncClass();
_ = asyncClass.GetNextValueAsync(); // fire and forget
var result1 = await asyncClass.GetNextValueAsync();
Console.WriteLine(result1);

Мы получим что-то вроде состояния гонки, поэтому результат будет либо 43, либо 44, и, кроме того, мы получим необработанный InvalidOperationException в нашей Task.Run лямбде. Первая лямбда устанавливает результаты (может быть, по принципу «выстрелил и забыл»), а вторая сталкивается с завершенной задачей, которую невозможно снова вызвать SetResult.

 Task.Run(() => {
        Thread.Sleep(1000);
        valueTaskSource.manualResetTask.SetResult(captureCounter);
    });

Конечно, разработчик мог бы создать пул valueTaskSources вместо одного, тогда проблемы бы не возникло:

public class SimpleValueTaskSourceImplementer<T> : IValueTaskSource<T> {
    public ManualResetValueTaskSourceCore<T> manualResetTask = new();
    public bool IsAvailable { get; set; } = true;

    public T GetResult(short token) {
        this.IsAvailable = true;
        return manualResetTask.GetResult(token);
    }
    public ValueTaskSourceStatus GetStatus(short token) {
        return manualResetTask.GetStatus(token);
    }
    public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) {
        manualResetTask.OnCompleted(continuation, state, token, flags);
    }
}

public class AsyncClass {
    public List<SimpleValueTaskSourceImplementer<int>> listValueTaskSources =
     new List<SimpleValueTaskSourceImplementer<int>>() { new() };

    object _lockObj = new object();

    public SimpleValueTaskSourceImplementer<int> GetAvailableBackingStore() {
        lock (_lockObj) {
            var firstAvailable = listValueTaskSources.FirstOrDefault(mr => mr.IsAvailable);
            if (firstAvailable != default) {
                firstAvailable.IsAvailable = false;
                return firstAvailable;
            }

            var newCreated = new SimpleValueTaskSourceImplementer<int>();
            newCreated.IsAvailable = false;
            listValueTaskSources.Add(newCreated);
            return newCreated;
        }
    }

    int m_counter = 42;

    public ValueTask<int> GetNextValueAsync() {
        var vts = GetAvailableBackingStore();
        vts.manualResetTask.Reset();
        Interlocked.Increment(ref m_counter);
        var captureCounter = m_counter;
        Task.Run(() => {
            Thread.Sleep(1000);
            vts.manualResetTask.SetResult(captureCounter);
        });

        return new ValueTask<int>(vts, vts.manualResetTask.Version);
    }
} 

Итак, я думаю, что это потенциальная проблема, но в большинстве случаев маловероятная.

Например, можно ли будет вернуть состояние поддержки в пул, даже если никто не ждал результата?

Это опять же зависит от исполнителя. Но я думаю, что этот комментарий в исходном коде предполагает, что обычной практикой является возврат экземпляра резервного состояния в пул в методе IValueTaskSource.GetResult:

После получения результата экземпляра ValueTask не попытайтесь получить его снова. Экземпляры ValueTask могут поддерживаться Экземпляры «IValueTaskSource», которые можно использовать повторно, и такие экземпляры могут использовать процесс получения результата экземпляров в качестве уведомления что экземпляр теперь может быть повторно использован для другой операции. Попытка затем повторно использовать ту же задачу ValueTask приводит к неопределенному результату. поведение.

Итак, в вашем случае, когда мы, естественно, не вызываем GetResult, потому что мы этого не делаем await, мы могли бы навсегда потратить экземпляр valueTaskSources для пула. Необходимо создать новый, старый, вероятно, продолжит работу, вызывая небольшую утечку памяти. Мы можем продемонстрировать это на нашем примере:

var asyncClass = new AsyncClass();
Console.WriteLine(asyncClass.listValueTaskSources.Count); // start with 1
await asyncClass.GetNextValueAsync();
await asyncClass.GetNextValueAsync();
Console.WriteLine(asyncClass.listValueTaskSources.Count); // still 1
_ = asyncClass.GetNextValueAsync();
_ = asyncClass.GetNextValueAsync();
_ = asyncClass.GetNextValueAsync();
await asyncClass.GetNextValueAsync();
Console.WriteLine(asyncClass.listValueTaskSources.Count); // 4 unfortunately

Я думаю, это немного похоже на утечки IAsyncResult в старом стиле APM, когда к ним не вызывался EndInvoke. В целом не здорово, но, на мой взгляд, ничего серьезного.

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

Joshua 05.07.2024 02:56
Ответ принят как подходящий

Вам нужно дождаться ValueTask?

Нет. Не обязательно.

Никогда не жду. Может ли это вызвать какие-либо проблемы?

Да, потенциально.

  1. Общие проблемы с «выстрелил и забыл», например, неизвестно, произошло ли исключение, или не известно, завершилась ли операция вообще. Например, в приложении ASP.NET рабочий процесс может быть перезапущен в любой момент, резко завершая любые ожидающие операции, которые были запущены по принципу «выстрелил и забыл».
  2. Проблемы с параллелизмом. Многие компоненты оснащены асинхронными API, но не поддерживают параллелизм. Если вы запустите асинхронную операцию до завершения предыдущей операции, поведение компонента будет неопределенным (обычно выдается исключение). Примером такого компонента является класс Stream, оснащенный API ReadAsync и WriteAsync. Не дожидаясь ValueTask, вы не можете знать, когда можно будет безопасно приступить к следующей операции с тем же экземпляром компонента.
  3. Проблемы с эффективностью памяти. API, возвращающие ValueTask, часто реализуются с резервным состоянием IValueTaskSource , которое объединяется в пул и используется повторно, что минимизирует выделение ресурсов и снижает нагрузку на сборщик мусора. Не ожидая ValueTask, компонент не имеет возможности узнать, что ValueTask использован, поэтому он не может повторно использовать резервное состояние. В результате будет создано новое состояние резервного копирования для следующей операции. Не существует формального способа сказать компоненту: «Я обещаю никогда await этого ValueTask». Все реализации IValueTaskSource, которые я видел, используют метод GetResult в качестве индикатора того, что ValueTask использован. GetResult вызывается автоматически инфраструктурой .NET всякий раз, когда ожидается ValueTask.

Спасибо. Это почти может стать основанием для нового вопроса: всегда ли GetResult() является разрушительной операцией? То есть, скажем, вы регистрируете GetAwaiter().OnCompleted(() => SomeCallback()), затем какой-то обратный вызов ждет десять часов и только затем вызывает GetResult() - это было бы глупой, но законной операцией, потому что ожидание завершения не гарантирует не делать недействительной задачу для дальнейших операций, а только потом слить результат? (сильно предполагаю, что это так, но здесь трудно найти документацию)

Bogey 05.07.2024 09:54

@​Bogey Я не думаю, что вы можете вернуть в пул экземпляр IValueTaskSource до вызова GetResult, потому что в этом случае экземпляр может быть преждевременно назначен другой асинхронной операции. Звонок GetResult не носит чисто декоративного характера. Его цель — выявить исключение, которое может храниться внутри ValueTask, вызвав его. Если вы вернете резервное состояние в пул, эта информация может быть потеряна (она обязательно будет потеряна, если реализация вызовет Reset перед возвратом использованного IValueTaskSource в пул).

Theodor Zoulias 05.07.2024 10:50

Документация: ManualResetValueTaskSourceCore<TResult>.Reset и исходный код.

Theodor Zoulias 05.07.2024 10:51

Другие вопросы по теме