Task vs ValueTask - The what's, when's and how's

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:

  1. It itselfs wraps TResult and Task<TResult>
  2. In contrast to Task ValueTask is a readonly struct instead of a class

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 ValueTasks 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.

3
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x