ValueTask - правильное использование

прочитал статью про ValueTask - возникло пару вопросов
вот код из статьи:

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

оно вручную опрашивает, не закончился ли таск и если закончился, то делает все синхронно; не закончился - асинхронно.

и кое-что не понял:

  • что это за странный метод _connection.RegisterCancellation()
  • и разве компилятор не может сам вместо слова await сделать разветвление на синхронный и асинхронный случай. может, в новых версиях? может, специально такого в компилятор не засовывают?

Ответы (1 шт):

Автор решения: aepot

Ваш вопрос можно разбить на 2 части

  • Task vs ValueTask
  • синхронное завершение метода vs асинхронное

Сразу скажу, технически эти 2 части вопроса между собой никак не связаны, но связаны логически.

Будет код синхронный или асинхронный решает не компилятор, а вы. Чтобы внедрить асинхронную машину состояний в метод, вы явно указываете ключевое слово async. А чтобы добавить состояния в эту машину, вы добавляете await.

Если на пути выполнения метода встретился await, то метод выполнится асинхронно в случае если await получит из вызванного метода незавершенный Task/ValueTask. В противном случае метод выполнится синхронно как обычный синхронный метод.

Самый простой пример такого ветвления - узнать свой IP:

class Program
{
    private static readonly HttpClient client = new HttpClient();
    private static string ipCache = null;

    static async Task Main(string[] args)
    {
        await GetIP();
        await GetIP();
        await GetIP();
        await GetIP();

        Console.ReadKey();
    }

    private static async Task GetIP()
    {
        Task<string> task = GetMyIP();
        Console.WriteLine(task.IsCompleted);
        if (!task.IsCompleted)
            await task;
    }

    private static async Task<string> GetMyIP()
    {
        if (ipCache == null)
        {
            string json = await client.GetStringAsync("https://api.myip.com/");
            ipCache = JsonSerializer.Deserialize<Response>(json).ip;
        }
        return ipCache;
    }
}

public class Response
{
    public string ip { get; set; }
}

Если в этом коде все Task поменять на ValueTask (кроме Main) - ничего не изменится.

Вывод в консоль

False
True
True
True

Здесь первый вызов метода завершился асинхронно, а остальные синхронно.

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

Итого, вам не стоит сильно запариваться о том, что там выдумал компилятор, но следует различать синхронное и асинхронное выполнение метода.

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

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

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

→ Ссылка