Recap
If you need a small refresher how Task or better async
/ await
works I got you covered.
Here my blog post or here the YouTube video from the talk itself.
In the later part of the talk I advise against using ValueTask
. Especially because there are some pitfalls you have to be careful of. Time passed and I re-advised my own comment / advise. In short to a certain degree I still stand against using ValueTask
everywhere but there are use-cases and depending on the usage it is totally fine.
What is a ValueTask
Let's oversimplify for the first part. How does ValueTask
look like?
public readonly struct ValueTask<TResult>
{
internal readonly Task<TResult> _task;
internal readonly TResult _result;
}
Now there are two things, which are important:
- It itselfs wraps
TResult
andTask<TResult>
- In contrast to
Task
ValueTask
is areadonly struct
instead of aclass
Let's unwrap that one by one
Wrapping TResult
and Task<TResult>
The idea behind ValueTask
is that you can await asynchronous and synchronous stuff. Here a small code sample:
public class WeatherService
{
private readonly Dictionary<string, WeatherData> _weatherCache;
private readonly IWeatherRepository weatherRepository;
public async ValueTask<WeatherData> GetWeatherForCityAsync(string city)
{
if (_weatherCache.ContainsKey(city))
{
return _weatherCache[city];
}
return await weatherRepository.GetForCityAsync(city);
}
}
We have two code path. One is asynchronous (when we have to call the WeatherRepository
) and one is synchronous (when we hit the cache).
That is the classic use case for ValueTask
. In some execution paths we don't have any asynchronous call and therefore we don't want to have the overhead of the Task-object. That is why ValueTask
has both fields, one Task<T>
for an asynchronous call-path and T
for the synchronous code path.
But why avoiding Task.FromResult
and introducing a whole new class / concept? That brings us directly to the second point
readonly struct
for ValueTask
Taking the example from before. Just imagine we're calling await GetWeatherForCityAsync("Zurich")
and our cache already holds the data?
Well we avoid the allocation of Task
.
And that is the whole point. We are avoiding allocation of a class. We all know that doesn't bring that much boost in performance. And that is why also Microsoft's state that ValueTask
is mainly meant for "hot paths".
This is why ValueTask
was added to .NET Core 2.0, and why new methods that are expected to be used on hot paths are now defined to return ValueTask instead of Task . For example, when we added a new ReadAsync overload to Stream in .NET Core 2.1 in order to be able to pass in a Memory instead of a byte[], we made the return type of that method be ValueTask .
Source can be found here
Now that sounds good so far. But our story doesn't end here. There are some pitfalls with ValueTask
.
Asynchronous hot paths
Now the stuff we discussed until now helps optimizing code which runs eventually synchronous but the .NET team went further and also optimized the allocation in the asynchronous call path. We know that a Task
object has to be created on the heap as it is a class
. But this doesn't mean that every awaitable Task has to be created newly. And there is where the magic of ValueTask
kicks in a second tine. Again it is all about optimizing hot paths.
Big No-No's
The following operations should never be performed on a ValueTask instance:
Awaiting the instance multiple times
We saw that ValueTask
s get recycled. Awaiting multiple times could mean you await a recycled ValueTask
with unknown outcome
ValueTask<WeatherData> weatherData = weatherService.GetWeatherForCityAsync("Zurich");
var result1 = await weatherData;
var result2 = await weatherData;
Awaiting the same instance concurrently
Also because of internal design ValueTask
only expects a single callback
ValueTask<WeatherData> weatherData = weatherService.GetWeatherForCityAsync("Zurich");
Task.Run(async() => await weatherData);
Task.Run(async() => await weatherData);
Using .Result or .GetAwaiter().GetResult() when the operation hasn't yet completed, or using them multiple times
Not used so often, but still this will lead to big problems.
When to use ValueTask
Via default and if in doubt use Task
. ValueTask
is meant for hot paths. I personally use it also in some Repository patterns with multiple implementations.
In fact that very page you are looking at calls a ValueTask
function to get the blog post content due to a cached repository which has sync
and async
code paths.
interface IRepository<T>
{
ValueTask<T> GetAll();
}
Summarizing:
If you for sure know that your caller will only await
your ValueTask
once, it is also fine to offer that Type.
If you need the allocation free version due to your requirements.
If you expect synchronous and asynchronous completion in your code.
But in general I would always check your code with a benchmark first. Don't add ValueTask
just because you could save some byte and a few nano seconds. You can introduce serious bugs. Furthermore if you offer an abstract class
or interface
account for inheritance and other factors. When in doubt take Task
.