ValueTask: Zero-Allocation Async Operations
August 27, 2024
ValueTask<T>
is a struct-based alternative to Task<T>
that eliminates allocations when operations complete synchronously or can be pooled.
Available since: .NET Core 2.0 / .NET Standard 2.1 / C# 7.0
Basic Concept
// Traditional Task - always allocates
public async Task<string> GetFromCacheAsync(string key)
{
return _cache.TryGetValue(key, out string value) ? value : await LoadFromDatabaseAsync(key);
}
// ValueTask - zero allocation when cache hits
public async ValueTask<string> GetFromCacheAsync(string key)
{
return _cache.TryGetValue(key, out string value) ? value : await LoadFromDatabaseAsync(key);
}
Synchronous Completion
public ValueTask<int> ReadAsync(Memory<byte> buffer)
{
if (_bytesAvailable > 0)
{
// Synchronous path - no allocation
int bytesRead = Math.Min(buffer.Length, _bytesAvailable);
_buffer.AsSpan(0, bytesRead).CopyTo(buffer.Span);
_bytesAvailable -= bytesRead;
return new ValueTask<int>(bytesRead);
}
// Asynchronous path - allocation occurs here
return ReadAsyncCore(buffer);
}
private async ValueTask<int> ReadAsyncCore(Memory<byte> buffer)
{
await WaitForDataAsync();
return ReadAsync(buffer).Result;
}
IValueTaskSource for Pooling
public sealed class PooledValueTask<T> : IValueTaskSource<T>
{
private static readonly ConcurrentQueue<PooledValueTask<T>> s_pool = new();
private ManualResetValueTaskSourceCore<T> _core;
public static PooledValueTask<T> Rent()
{
return s_pool.TryDequeue(out var task) ? task : new PooledValueTask<T>();
}
public ValueTask<T> Task => new ValueTask<T>(this, _core.Version);
public void SetResult(T result)
{
_core.SetResult(result);
}
public void SetException(Exception error)
{
_core.SetException(error);
}
public T GetResult(short token) => _core.GetResult(token);
public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _core.OnCompleted(continuation, state, token, flags);
private void Return()
{
_core.Reset();
s_pool.Enqueue(this);
}
}
ConfigureAwait(false) Still Matters
// Library code - avoid deadlocks
public async ValueTask<Data> ProcessDataAsync()
{
var raw = await LoadDataAsync().ConfigureAwait(false);
var processed = await TransformAsync(raw).ConfigureAwait(false);
return processed;
}
Converting Between Task and ValueTask
// ValueTask to Task (when needed for Task.WhenAll, etc.)
ValueTask<string> valueTask = GetDataAsync();
Task<string> task = valueTask.AsTask();
// Task to ValueTask (implicit)
Task<string> task = LoadFromDatabaseAsync();
ValueTask<string> valueTask = task; // implicit conversion
Hot Path Optimization
private readonly Dictionary<string, string> _cache = new();
public ValueTask<string> GetValueAsync(string key)
{
// Fast path - synchronous, zero allocation
if (_cache.TryGetValue(key, out string cached))
return new ValueTask<string>(cached);
// Slow path - asynchronous, allocation
return LoadAndCacheAsync(key);
}
private async ValueTask<string> LoadAndCacheAsync(string key)
{
string value = await _repository.LoadAsync(key);
_cache[key] = value;
return value;
}
State Machine Considerations
// DON'T: Multiple awaits on same ValueTask
var valueTask = GetDataAsync();
string result1 = await valueTask; // OK
string result2 = await valueTask; // WRONG - undefined behavior
// DO: Use .Preserve() if you need multiple awaits
var valueTask = GetDataAsync();
Task<string> task = valueTask.Preserve();
string result1 = await task; // OK
string result2 = await task; // OK
Performance Pattern
public ValueTask<bool> TryProcessAsync()
{
// Check if we can complete synchronously
if (CanProcessSynchronously())
{
ProcessSynchronously();
return new ValueTask<bool>(true);
}
// Fall back to async processing
return ProcessAsyncCore();
}
ValueTask<T>
reduces pressure on the garbage collector in high-throughput scenarios where many operations complete synchronously, particularly effective in caching layers and I/O abstractions.