0Pricing
C# Academy · Урок

ValueTask и предотвращение выделений памяти

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

«ValueTask и предотвращение выделений памяти» — бесплатный урок C# Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.

Проблема выделения памяти при использовании Task

Task<T> — ссылочный тип: каждый async-метод выделяет Task в куче, даже если результат доступен синхронно (например, из кэша). В часто выполняемых участках это может существенно увеличить нагрузку на сборщик мусора. ValueTask<T> решает эту проблему.

Что такое ValueTask?

ValueTask<T> — это структура, представляющая либо доступный синхронно результат, либо ожидающую асинхронную операцию. Если результат синхронный, выделение памяти в куче не требуется.

// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
    _cache.TryGetValue("key", out var v) ? v : await FetchAsync();

// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
    _cache.TryGetValue("key", out var v)
        ? ValueTask.FromResult(v)  // no allocation
        : new ValueTask<string>(FetchAsync()); // allocation only when needed

Возврат ValueTask из интерфейса

Используйте ValueTask<T> в методах интерфейса, которые вызываются часто и нередко завершаются синхронно. Consumers работают с ним так же, как с Task<T>.

public interface IProductRepository
{
    // ValueTask: often completes synchronously from cache
    ValueTask<Product?> GetByIdAsync(int id);

    // Task: always async (DB query)
    Task<List<Product>> SearchAsync(string query);
}

// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");

ValueTask с IValueTaskSource

Для максимальной производительности реализуйте IValueTaskSource<T>, чтобы повторно использовать объект, хранящий состояние операции. Это устраняет все выделения памяти даже для асинхронных завершений и используется внутри .NET в коде для сокетов и операций ввода-вывода.

// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.

// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughput

Ограничения ValueTask: ожидайте только один раз

Критически важное правило: ValueTask<T> можно ожидать не более одного раза, а вызывать .Result до завершения нельзя. Нарушение этого правила приводит к неопределённому поведению.

var vt = repo.GetByIdAsync(42);

// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!

// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG

// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple times

Как выбрать между ValueTask<T> и Task<T>

Используйте ValueTask<T> только если профилирование показывает, что async-методы создают чрезмерную нагрузку из-за выделения памяти. Не применяйте его повсюду: в редко вызываемом коде он лишь усложняет программу и не даёт преимуществ.

// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O

// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share it

ValueTask без обобщения

Необобщённый ValueTask предназначен для методов, возвращающих Task (асинхронный метод без результата). Используйте его в часто вызываемых методах, которые нередко завершаются синхронно.

public interface ICache
{
    // Often a no-op when value exists — ValueTask avoids allocation
    ValueTask SetAsync(string key, string value);
    ValueTask InvalidateAsync(string key);
}

public class InMemoryCache : ICache
{
    private readonly Dictionary<string, string> _data = new();

    public ValueTask SetAsync(string key, string value)
    {
        _data[key] = value;
        return ValueTask.CompletedTask; // no allocation
    }
}

Измерение выделения памяти с помощью BenchmarkDotNet

Используйте [MemoryDiagnoser] в BenchmarkDotNet, чтобы измерять выделение памяти на операцию. Это единственный надёжный способ проверить, действительно ли ValueTask помогает.

[MemoryDiagnoser]
public class CacheBenchmark
{
    private readonly CachedRepo _repo = new();

    [Benchmark]
    public async Task<Product?> Task_GetById()
        => await _repo.GetByIdTask(1);

    [Benchmark]
    public async ValueTask<Product?> ValueTask_GetById()
        => await _repo.GetByIdValueTask(1);
}

// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hit

ValueTask в SocketsHttpHandler

Стек HTTP в .NET активно использует ValueTask внутри. Понимание этого помогает писать высокопроизводительные HTTP-клиенты, согласованные со стратегией управления памятью среды выполнения.

// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/O

Практический пример: кэш-сервис для часто выполняемого участка

В кэш-сервисе каждый вызов потенциально может попасть в кэш — ValueTask устраняет выделения памяти на часто выполняемом участке и при этом поддерживает асинхронный fallback при отсутствии данных в кэше.

public class ProductCache
{
    private readonly Dictionary<int, Product> _cache = new();
    private readonly IProductRepository _repo;

    public ProductCache(IProductRepository repo) => _repo = repo;

    public ValueTask<Product?> GetAsync(int id)
    {
        // Cache hit: no allocation
        if (_cache.TryGetValue(id, out var cached))
            return ValueTask.FromResult<Product?>(cached);

        // Cache miss: async fetch
        return new ValueTask<Product?>(FetchAndCacheAsync(id));
    }

    private async Task<Product?> FetchAndCacheAsync(int id)
    {
        var product = await _repo.GetByIdAsync(id);
        if (product is not null) _cache[id] = product;
        return product;
    }
}

Быстрая проверка

Какое главное правило действует для ожидания ValueTask?

Повторение: ValueTask и предотвращение выделений памяти

Основные выводы:

  • ValueTask<T>: структура, избегающая выделения памяти в куче, когда результат доступен синхронно
  • Лучше всего подходит для часто выполняемых участков, которые часто завершаются синхронно (например, при попадании в кэш)
  • NEVER ожидайте ValueTask<T> более одного раза — используйте .AsTask(), чтобы разделить результат
  • Используйте [MemoryDiagnoser], чтобы проверить экономию памяти перед оптимизацией
  • Для общего применения предпочитайте Task<T>; используйте ValueTask<T> только при наличии обоснования по результатам профилирования

Часто задаваемые вопросы

Урок «ValueTask и предотвращение выделений памяти» бесплатный?

Да — полный текст урока «ValueTask и предотвращение выделений памяти» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.

Чему я научусь в уроке «ValueTask и предотвращение выделений памяти»?

Используйте ValueTask, чтобы уменьшить выделения в куче на часто выполняемых участках, учитывайте его ограничения и избегайте распространённых ошибок применения. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C# Academy?

Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «ValueTask и предотвращение выделений памяти»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C# Academy?

Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. IAsyncEnumerable и await foreach
  2. System.Threading.Channels
  3. ValueTask и предотвращение выделений памяти
  4. ConfigureAwait и контекст синхронизации
← Назад к C# Academy