Asynchronous Programming – ValueTask

By | 15/03/2023

In this post, we will see what ValueTask is and how to use it in our projects.
But first of all, what is ValueTask?
From Microsoft web site:
“A ValueTask is a structure that can wrap either a Task or a IValueTaskSource instance. Returning a ValueTask that wraps a IValueTaskSource instance from an asynchronous method enables high-throughput applications to avoid allocations by using a pool of reusable IValueTaskSource objects”.
In a nutshell, we can use ValueTask when we have a method that can return either synchronously or asynchronously. It is a struct so it means that it doesn’t write in the heap but in the stack and this will improve not only the memory consumption of the method but, the executing performance as well.
It is important to highlight that we should use the ValueTask only if the method returns synchronously at least 90% of time otherwise, it is better using a Task.
Furthermore, there are some limitation with the ValueTask and two of them are:
1) Never do multiple await operations on the same method
2) Never do a parallel call concurrently

Now, we will see an example how to use ValueTask.
We open Visual Studio, we create a Console Application and then we add two files:

[CORE.CS]
This class is used to simulate a Business Layer.

namespace TestValueTask;

public class Core
{
    List<string> lstString = new List<string>();

	public Core(bool isListLoaded)
	{
		// This is used to simulate when data is already loaded
		if(isListLoaded)
		{
			for (int i = 1; i < 1000; i++)
			{
                lstString.Add(i.ToString());
			}
		}
	}

	// We create two equal methods but using two different outputs:
	// one Task and the other one ValueTask

	public async Task<List<string>> GetList()
	{
		if(lstString.Count>0)
		{
			return lstString;
		}
		else
		{
			return await GetDataFromMemory();
		}
		
	}

    public async ValueTask<List<string>> GetListValueTask()
    {
        if (lstString.Count > 0)
        {
            return lstString;
        }
        else
        {
            return await GetDataFromMemory();
        }

    }

    private async Task<List<string>> GetDataFromMemory()
	{
        for (int i = 1; i < 10000; i++)
        {
            lstString.Add(i.ToString());
        }


        return await Task.FromResult(lstString);
	}
}


[BENCHMARKCORE.CS]
This class is used to run the Benchmark.

using BenchmarkDotNet.Attributes;

namespace TestValueTask;

[MemoryDiagnoser] 
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] 
[RankColumn]
public class BenchmarkCore
{
    private Core? objCoreWithData;
    private Core? objCoreWithoutData;

    [GlobalSetup] 
    public void GlobalSetup()
    {
        // we create two instances of the same class:
        // one with data loaded and the other one without data
        objCoreWithData = new Core(true);
        objCoreWithoutData = new Core(false);
    }


    [Benchmark] 
    public async Task TestMethodGetListWithoutData_Task()
    {
        var result = await objCoreWithoutData.GetList();

    }

    [Benchmark] 
    public async Task TestMethodGetListWithData_Task()
    {
        var result = await objCoreWithData.GetList();
    }

    [Benchmark]
    public async Task TestMethodGetListWithoutData_ValueTask()
    {
        var result = await objCoreWithoutData.GetListValueTask();

    }

    [Benchmark]
    public async Task TestMethodGetListWithData_ValueTask()
    {
        var result = await objCoreWithData.GetListValueTask();
    }
}


Then, we modify the file Program.cs:
[PROGRAMM.CS]

using BenchmarkDotNet.Running;
using TestValueTask;

Console.WriteLine("Start Benchmark");

BenchmarkRunner.Run<BenchmarkCore>();


Finally, we run the Benchmarks:

In this case we haven’t had a variation in terms of speed but, in the methods where we used TaskValue, we haven’t had memory allocated.


Leave a Reply

Your email address will not be published. Required fields are marked *