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 itValueTask без обобщения
Необобщённый 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 hitValueTask в 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 — локальная установка не требуется.
Все уроки этого курса
- IAsyncEnumerable и await foreach
- System.Threading.Channels
- ValueTask и предотвращение выделений памяти
- ConfigureAwait и контекст синхронизации