Skip to content

Why Should I use ValueTask in C#?

A Task represents the state of some operation, i.e., whether the process is completed, cancelled, etc. An asynchronous method can return either a Task or a ValueTask.

Since Task is a reference type, returning a Task object from an asynchronous function requires the object to be allocated on the managed heap each time the method is invoked. Each time you return a Task object from a method, you must allocate memory on the managed heap. If the outcome of the action being done by your method is instantly accessible or completed synchronously, then this allocation is unnecessary and hence expensive.

Exactly at this point does ValueTask come to the rescue. ValueTask<T> offers two key advantages. First, ValueTask<T> increases efficiency since it does not require heap allocation, and second, its implementation is both simple and adaptable. By returning ValueTask<T> instead of Task<T> from an asynchronous method when the result is immediately available, you can avoid the unnecessary allocation overhead because “T” here represents a structure and a structure in C# is a value type (as opposed to the “T” in Task<T>, which represents a class).

Task and ValueTask are the two basic forms of “awaitable” in C#. Note that ValueTasks cannot be blocked. If you need to block, you must first convert the ValueTask to a Task using the AsTask function, and then block on the Task object referenced by the ValueTask.

Note that each ValueTask can only be utilized once. Here, “consume” suggests that a ValueTask can asynchronously wait for (await) the action to complete or utilize AsTask to turn a ValueTask into a Task. However, a ValueTaskT> should only be consumed once before being disregarded.

ValueTask example in C#

				
					public Task<int> GetCustomerIdAsync()
{
    return Task.FromResult(1);
}
				
			
The above code fragment does not generate whole async state machine, but it does allocate Task object on the managed memory. 
To prevent this allocation, you may choose to utilize ValueTask instead, as seen in the code below.
				
					public ValueTask<int> GetCustomerIdAsync()
{
    return new ValueTask(1);
}
				
			

The following code snippet illustrates a synchronous implementation of ValueTask.

				
					 public interface IRepository<T>
    {
        ValueTask<T> GetData();
    }
				
			

The Repository class extends the IRepository interface and implements its methods as shown below.

				
					 public class Repository<T> : IRepository<T>
    {
        public ValueTask<T> GetData()
        {
            var value = default(T);
            return new ValueTask<T>(value);
        }
    }
				
			

Here is how you can call the GetData method from Main method.

				
					static void Main(string[] args)
        {
            IRepository<int> repository = new Repository<int>();
            var result = repository.GetData();
            if(result.IsCompleted)
                 Console.WriteLine("Operation complete...");
            else
                Console.WriteLine("Operation incomplete...");
            Console.ReadKey();
        }
				
			

Let’s now add another asynchronous function to our repository, GetDataAsync. Here is how the IRepository interface would be altered.

				
					public interface IRepository<T>
    {
        ValueTask<T> GetData();
        ValueTask<T> GetDataAsync();
    }
				
			

The GetDataAsync method is implemented by the Repository class as shown in the code snippet given below.

				
					    public class Repository<T> : IRepository<T>
    {
        public ValueTask<T> GetData()
        {
            var value = default(T);
            return new ValueTask<T>(value);
        }
        public async ValueTask<T> GetDataAsync()
        {
            var value = default(T);
            await Task.Delay(100);
            return value;
        }
    }
				
			

When should ValueTask be used in C#?

Despite the benefits provided by ValueTask, there are drawbacks to adopting ValueTask instead of Task. Task is a reference type with a single field, whereas ValueTask is a value type with two fields. Using a ValueTask necessitates working with extra data, as a method call returns two fields of data rather than one. In addition, if you await a method that returns a ValueTask, the state machine for that asynchronous function would be bigger, as it would need to handle a struct with two fields rather than a single reference as in the case of a Task.

In addition, if the consumer of an asynchronous method makes use of Task. WhenAll or Activity. Using ValueTaskT> as a return type in an asynchronous function might become expensive when WhenAny is true. This is because you would need to utilize the AsTask function to convert ValueTaskT> to TaskT>, which would result in an allocation that could have been easily avoided if a cached TaskT> had been used in the first place.

Here is the general rule of thumb. Utilize Task when your code will always be asynchronous, i.e., when the action will not complete immediately. Utilize ValueTask when the result of an asynchronous action is already accessible or when a cached result is already available. Before considering ValueTask, you must complete the appropriate performance analysis.

 

Leave a Reply

Your email address will not be published.