Сегодня мы будем разбирать интересную вещь в C# ValueTask — штука, которая спасет асинхронные методы от лишних аллокаций.
Если коротко, ValueTask — это структура, которая позволяет вернуть либо Task, либо готовый результат. Она появилась в C# 7.0 для снижения накладных расходов при работе с асинхронным кодом.
Простой пример:
public ValueTask<int> GetMagicNumberAsync()
{
if (DateTime.Now.Second % 2 == 0)
{
// Результат готов — возвращаем синхронно
return new ValueTask<int>(42);
}
// Асинхронный результат
return new ValueTask<int>(Task.Run(() => CalculateMagicNumber()));
}
Без ValueTask вы всегда возвращаете объект Task, который создаётся даже для синхронных случаев. А это лишняя аллокация. С ValueTask вы возвращаете либо готовое значение, либо уже существующий Task, экономя ресурсы.
public class CatService
{
private readonly Dictionary<int, string> _catCache = new();
public async ValueTask<string> GetCatAsync(int id)
{
if (_catCache.TryGetValue(id, out var name))
{
return name; // Возвращаем синхронно
}
name = await FetchCatFromDatabaseAsync(id); // Эмуляция долгой операции
_catCache[id] = name; // Кэшируем результат
return name;
}
private async Task<string> FetchCatFromDatabaseAsync(int id)
{
await Task.Delay(100); // Считаем, что это запрос в базу
return $"Котик с ID {id}";
}
}
Если котик в кеше, мы возвращаем результат сразу, без лишних аллокаций. Если данных нет, мы идём в базу, а затем кэшируем результат.
1. Нельзя ждать один и тот же ValueTask дважды
var task = service.GetCatAsync(1);
await task;
await task; // ValueTask больше так не работает.
ValueTask можно "await-ить" только один раз. Если надо несколько — преобразуйте в Task:
var task = service.GetCatAsync(1).AsTask();
await task;
await task; // Теперь работает.
2. Использование .Result до завершения операции
var task = service.GetCatAsync(1);
Console.WriteLine(task.Result); // Исключение: результат ещё не готов.
ValueTask нельзя читать через .Result, пока он не завершён. Это приведёт к ошибке.
3. Неправильное использование конструктора
public ValueTask<int> BrokenTask()
{
return new ValueTask<int>(Task.CompletedTask); // Ошибка: Task не возвращает TResult.
}
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class CatStore
{
private readonly ConcurrentDictionary<int, string> _cache = new();
private readonly SemaphoreSlim _lock = new(1, 1);
public async ValueTask<string> GetCatNameAsync(int id, CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(id, out var cachedName))
{
LogInfo($"Котик с ID {id} найден в кеше.");
return cachedName;
}
await _lock.WaitAsync(cancellationToken);
try
{
if (_cache.TryGetValue(id, out cachedName))
{
LogInfo($"Котик с ID {id} найден в кеше после блокировки.");
return cachedName;
}
var fetchedName = await FetchFromDatabaseAsync(id, cancellationToken);
_cache.TryAdd(id, fetchedName);
return fetchedName;
}
catch (Exception ex)
{
LogError($"Ошибка при получении данных о котике с ID {id}: {ex.Message}");
throw;
}
finally
{
_lock.Release();
}
}
private async Task<string> FetchFromDatabaseAsync(int id, CancellationToken cancellationToken)
{
try
{
await Task.Delay(100, cancellationToken);
LogInfo($"Запрос в базу данных для котика с ID {id}.");
return $"Котик с ID {id}";
}
catch (TaskCanceledException)
{
LogWarning($"Запрос в базу данных для котика с ID {id} был отменён.");
throw;
}
catch (Exception ex)
{
LogError($"Ошибка при запросе в базу данных: {ex.Message}");
throw;
}
}
private void LogInfo(string message) => Console.WriteLine($"[INFO]: {message}");
private void LogWarning(string message) => Console.WriteLine($"[WARNING]: {message}");
private void LogError(string message) => Console.WriteLine($"[ERROR]: {message}");
}
Теперь у нас потокобезопасный ConcurrentDictionary, который не сломается от одновременных запросов. Семафор бережет базу от двойных ударов, а логирование говорит, что происходит. Ну и бонус — поддержка отмены операций, если вдруг кто-то решит, что котик не стоит ожидания.
Если коротко, ValueTask — это структура, которая позволяет вернуть либо Task, либо готовый результат. Она появилась в C# 7.0 для снижения накладных расходов при работе с асинхронным кодом.
Простой пример:
public ValueTask<int> GetMagicNumberAsync()
{
if (DateTime.Now.Second % 2 == 0)
{
// Результат готов — возвращаем синхронно
return new ValueTask<int>(42);
}
// Асинхронный результат
return new ValueTask<int>(Task.Run(() => CalculateMagicNumber()));
}
Без ValueTask вы всегда возвращаете объект Task, который создаётся даже для синхронных случаев. А это лишняя аллокация. С ValueTask вы возвращаете либо готовое значение, либо уже существующий Task, экономя ресурсы.
Когда использовать ValueTask?
Есть три сценария, где ValueTask покажет себя во всей красе:- Синхронный результат в большинстве случаев. Например, чтение из кеша.
- Высокая нагрузка. Когда важно минимизировать аллокации и повысить производительность.
- Методы с высокой частотой вызовов. Например, в real-time системах.
Пример использования
Представим магазин котиков. У нас есть метод, который ищет данные о котике: если он в кеше — возвращаем сразу, если нет — идём в базу.public class CatService
{
private readonly Dictionary<int, string> _catCache = new();
public async ValueTask<string> GetCatAsync(int id)
{
if (_catCache.TryGetValue(id, out var name))
{
return name; // Возвращаем синхронно
}
name = await FetchCatFromDatabaseAsync(id); // Эмуляция долгой операции
_catCache[id] = name; // Кэшируем результат
return name;
}
private async Task<string> FetchCatFromDatabaseAsync(int id)
{
await Task.Delay(100); // Считаем, что это запрос в базу
return $"Котик с ID {id}";
}
}
Если котик в кеше, мы возвращаем результат сразу, без лишних аллокаций. Если данных нет, мы идём в базу, а затем кэшируем результат.
Частые грабли
ValueTask — инструмент мощный, но с ним легко наломать дров. Типичные ошибки:1. Нельзя ждать один и тот же ValueTask дважды
var task = service.GetCatAsync(1);
await task;
await task; // ValueTask больше так не работает.
ValueTask можно "await-ить" только один раз. Если надо несколько — преобразуйте в Task:
var task = service.GetCatAsync(1).AsTask();
await task;
await task; // Теперь работает.
2. Использование .Result до завершения операции
var task = service.GetCatAsync(1);
Console.WriteLine(task.Result); // Исключение: результат ещё не готов.
ValueTask нельзя читать через .Result, пока он не завершён. Это приведёт к ошибке.
3. Неправильное использование конструктора
public ValueTask<int> BrokenTask()
{
return new ValueTask<int>(Task.CompletedTask); // Ошибка: Task не возвращает TResult.
}
Теперь улучшим пример с котиками
Добавим асинхронный кеш, защиту от гонок и немного логики, чтобы не ломать голову, если что-то пойдет не так:using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class CatStore
{
private readonly ConcurrentDictionary<int, string> _cache = new();
private readonly SemaphoreSlim _lock = new(1, 1);
public async ValueTask<string> GetCatNameAsync(int id, CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(id, out var cachedName))
{
LogInfo($"Котик с ID {id} найден в кеше.");
return cachedName;
}
await _lock.WaitAsync(cancellationToken);
try
{
if (_cache.TryGetValue(id, out cachedName))
{
LogInfo($"Котик с ID {id} найден в кеше после блокировки.");
return cachedName;
}
var fetchedName = await FetchFromDatabaseAsync(id, cancellationToken);
_cache.TryAdd(id, fetchedName);
return fetchedName;
}
catch (Exception ex)
{
LogError($"Ошибка при получении данных о котике с ID {id}: {ex.Message}");
throw;
}
finally
{
_lock.Release();
}
}
private async Task<string> FetchFromDatabaseAsync(int id, CancellationToken cancellationToken)
{
try
{
await Task.Delay(100, cancellationToken);
LogInfo($"Запрос в базу данных для котика с ID {id}.");
return $"Котик с ID {id}";
}
catch (TaskCanceledException)
{
LogWarning($"Запрос в базу данных для котика с ID {id} был отменён.");
throw;
}
catch (Exception ex)
{
LogError($"Ошибка при запросе в базу данных: {ex.Message}");
throw;
}
}
private void LogInfo(string message) => Console.WriteLine($"[INFO]: {message}");
private void LogWarning(string message) => Console.WriteLine($"[WARNING]: {message}");
private void LogError(string message) => Console.WriteLine($"[ERROR]: {message}");
}
Теперь у нас потокобезопасный ConcurrentDictionary, который не сломается от одновременных запросов. Семафор бережет базу от двойных ударов, а логирование говорит, что происходит. Ну и бонус — поддержка отмены операций, если вдруг кто-то решит, что котик не стоит ожидания.
Нюансы (а как без них?)
- ValueTask хорош, если вы избегаете создания новых объектов. Но если вы всё равно преобразуете его в Task, профит теряется.
- Поскольку ValueTask — это структура, он копируется при передаче, что может быть дороже, чем работа с Task.
- С ValueTask надо работать аккуратно.
Как и зачем использовать ValueTask в C#
Привет, Хабр! Сегодня мы будем разбирать интересную вещь в C# ValueTask — штука, которая спасет асинхронные методы от лишних аллокаций. Если коротко, ValueTask — это структура, которая позволяет...
habr.com