ValueTask and Task - a Simple Explanation in Nutshell

If you already know of the Task type, and have now happened to meet a similar looking ValueTask, then you are likely interested in knowing more about these types, and this article is my attempt at a quick explanation of the relation between ValueTask and Task types.
(Rev. 19-Mar-2024)

Categories | About |     |  

Parveen,

The problem with Task<TResult>

Let's develop the concept step by step.

A function that returns a Task doesn't always execute asynchronously. It can sometimes finish synchronously - for example consider this simple function that doesn't execute asynchronously if x < 300.


public async Task<int> CalculateSquare(int x)
{
    if (x > 300)
    {
        await Task.Delay(1000);
    }

    return x * x;
}

The return type of this function is a Task ir-respective of the mode of completion. And this means that there is an un-necessary allocation of Task if the input x < 300.

Well, this is a simple example that I gave, but consider the in-efficiency associated with an ASP.NET Core application that has thousands of users calling async functions that happen to finish synchronously most of the time?

Before we come to the solution of this problem, we can agree on these three points:

  1. An async function that returns a Task can complete either synchronously, or asynchronously depending on the conditions of execution - and possibly depending also on the state of the underlying hardware.
  2. A Task allocation will always take place - irrespective of whether the function finishes synchronously or asynchronously. Each allocation takes place on the grabage collected heap because a Task is after all a class.
  3. A heavy traffic on these functions can lead to an avoidable load on the garbage collector, because some of these Task allocations were really un-necessary!

Video Explanation (see it happen!)

Please watch the following youtube video:

The solution by ValueTask<TResult>

An async code can be optimized by returning a ValueTask instead of a Task.

ValueTask is a struct that can hold three types of data - but only one of them is used at a time. The three types are held in various internal data members of this struct. Let's list them!

Please see the source code on github - Source Code of ValueTask.cs on Github

  1. internal readonly TResult? _result; if the operation completes synchronously.
  2. internal readonly object? _obj; stores a Task<TResult> for the asynchronous case
  3. internal readonly object? _obj; can also store an IValueTaskSource<TResult>, which was added later for pooling and re-use of the resources.

So we can easily see that a ValueTask can store the result directly to avoid the overhead of creating a Task. Hence, we can say, in nutshell, that a ValueTask is a multi-purpose struct that can avoid the overhead of creating a Task if an operation completes synchronously.

How is the data in a ValueTask determined?

The next question is how do we know which member contains the data of interest - it is in _obj or is it in _result? Here are the rules used internally by the struct:

Please see the source code on github - Source Code of ValueTask.cs on Github

if _obj is null, then the operation has completed synchronously, and the result is contained in _result.

if _obj is not null and another member called internal readonly short _token; is non-zero, then _obj contains an IValueTaskSource and _token contains a token associated with it.

if _obj is not null and internal readonly short _token; is zero, then _obj contains a Task

Limitations of a ValueTask

A ValueTask is recommended only if your code will await it once - and consider using a ValueTask if you expect heavy traffic on a function that returns a Task. For more detail please do refer to the msdn documentation, especially the famous article Understanding the Whys, Whats, and Whens of ValueTask (Stephen Toub) .

Using a ValueTask (see the linked video for a better clarity with breakpoints)

Let's see the things practically. Open the solution explorer, and locate the program.cs file. Double click to open it.

We have changed the return type of our function to a ValueTask. The function CalculateSquare executes asynchronously only if the input is more than 300. Otherwise it executes synchronously.


// see linked video for better understanding 

using System;
using System.Threading.Tasks;

public class Program
{
    public static async ValueTask<int> CalculateSquare(int x)
    {
        if (x > 300)
        {
            await Task.Delay(1000);
        }

        return x * x;
    }

    public static void Main()
    {
        // see linked video for better 
        ValueTask<int> vt = CalculateSquare(600);

        int sq = vt.Result;

        Console.WriteLine($"Square = {sq}");
    }
}

Inside Main we make a call to CalculateSquare and store the result in a variable vt, and then extract the result from vt to store it in an int.

Put a breakpoint as you see here, and run the code by passing a value of 50. The code will execute synchronously.

We observe that _result contains the output and _obj is null, which verifies that a Task wasn't allocated, and optimization has indeed taken place.

We can similarly run the program by passing a value of 600.

We observe that now _obj contains a Task because the operation was asynchronous.

Thus we have seen practically that a ValueTask can hold data for both the scenarios. Thankyou!


This Blog Post/Article "ValueTask and Task - a Simple Explanation in Nutshell" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.