Understanding ValueTask<T> in C#
A guide to the ValueTask<T> struct in C#. Learn how it can be used as a lightweight alternative to Task<T> to improve performance and reduce allocations in high-throughput, asynchronous scenarios.
In C#, the standard way to represent an asynchronous operation that returns a value is with Task<T>
. It's a powerful and flexible class that is at the heart of the async
/await
programming model. However, Task<T>
is a class, which means that every time you return one from an async
method, an object must be allocated on the heap.
In most applications, this small allocation is not a problem. But in high-performance, high-throughput scenarios—like a busy web server or a low-level networking library—these small allocations can add up, putting pressure on the garbage collector and impacting performance.
To solve this problem, C# introduced ValueTask<T>
.
What is ValueTask<T>
?
ValueTask<T>
is a struct
that is designed to be a lightweight alternative to Task<T>
. Because it's a struct, it is a value type, and in many cases, it can be allocated on the stack instead of the heap, avoiding a garbage collection.
ValueTask<T>
is actually a wrapper that can represent one of two things:
- A completed result of type
T
. - A
Task<T>
that represents an ongoing asynchronous operation.
The Key Use Case: Synchronous Completion
The primary scenario where ValueTask<T>
shines is when an async
method is likely to complete synchronously. A common example is reading from a buffer.
Imagine you are writing a method to read data from a network stream. You might have a buffer that already contains the data you need. In this case, you can return the data immediately without any actual asynchronous work.
With Task<T>
:
public Task<byte[]> ReadDataAsync()
{
if (_buffer.TryRead(out byte[] data))
{
// We have the data already, but we still have to allocate a Task
// to wrap the result.
return Task.FromResult(data);
}
else
{
// Go and do the actual async work
return ReadFromNetworkAsync();
}
}
Even in the synchronous path, Task.FromResult
still needs to allocate a Task
object.
With ValueTask<T>
:
public ValueTask<byte[]> ReadDataAsync()
{
if (_buffer.TryRead(out byte[] data))
{
// We have the data already. We can return it directly
// wrapped in a ValueTask, with no heap allocation.
return new ValueTask<byte[]>(data);
}
else
{
// Go and do the actual async work
return new ValueTask<byte[]>(ReadFromNetworkAsync());
}
}
In the synchronous completion path, we avoid the Task
allocation entirely. For a method that is called thousands of times per second, this can lead to a significant performance improvement.
How to Consume a ValueTask<T>
The good news is that from the caller's perspective, consuming a ValueTask<T>
is exactly the same as consuming a Task<T>
. You just await
it.
byte[] myData = await ReadDataAsync();
The compiler handles all the details for you.
Important Rules and When to Use It
ValueTask<T>
is an advanced optimization, not a general replacement for Task<T>
. There are some important restrictions on how you can use it:
- You can only
await
aValueTask<T>
once. Because it might be wrapping a reusable object, awaiting it a second time can lead to bugs. - Don't block on a
ValueTask<T>
. Calling.Result
or.GetAwaiter().GetResult()
can also lead to bugs. - Don't use it when the operation is always asynchronous. If your method will always involve a true asynchronous wait, you should just use
Task<T>
. The benefits ofValueTask<T>
only appear when the method can complete synchronously.
So, when should you use it?
- When you are writing a library or a piece of performance-critical code where you want to minimize allocations.
- When you have an
async
method that you expect will very often be able to return its result from a cache or buffer without any actual asynchronous I/O.
Conclusion
ValueTask<T>
is a powerful tool for optimizing asynchronous code in high-performance scenarios. By providing a way to return a result from an async
method without allocating a Task
on the heap, it can help you reduce pressure on the garbage collector and squeeze every last drop of performance out of your application.
For most everyday application code, Task<T>
is still the right choice. But for library authors and performance enthusiasts, ValueTask<T>
is an essential tool to have in your optimization toolkit.