Understanding Task Vs ValueTask

These days most of the Interfaces we are seeing with methods have a return type as ValueTask. So, through this article, we are going to understand what is ValueTask, how it is different from Task, and when do we need to use ValueTask?

Before addressing the above questions, we will deep dive into the below method.

public async Task < List < Employee >> GetEmployees() {
    if (_EmployeeCache == null) {
        _EmployeeCache = await _employeeRepo.GetEmployees();
        return employees;
    } else {
        return _EmployeeCache;
    }
}

With this method call, the caller will expect a list of employee information.  Also, this method is an Asynchronous call and returns a Task object. Till now everything is good, then what is wrong here? Let's understand it, for the first call _EmployeeCache object will be null and it does a repository call asynchronously to get the list of employees from the database and assign it to _EmployeeCache object. Once this object is assigned, from the next call it avoids this asynchronously repository call returns what is available in _EmployeeCache object.

When observed, this method most of the time does a synchronous call and the only case where we do an asynchronous call is when _EmployeeCache is null. But, for both the cases it creates the Task object on heap memory and returns it to the caller and it leads to unnecessary memory usage even when the object can directly return (i.e., EmployeeCache is not the NULL case).

ValueTask type will address such an issue. ValueTask is a struct type as below

public readonly struct ValueTask<TResult> : IEquatable<System.Threading.Tasks.ValueTask<TResult>>

When the method is ValueTask return type, the runtime will create a Task object when we do an asynchronous call and in the case of a synchronous call, it will directly return the result object without creating the Task object. By this implementation, we can see good performance improvement as well as memory utilization when we have a method where it has huge calls per request or process. Performance improves by avoiding unnecessary Task object process and memory utilization by avoiding unnecessary creation of objects on Heap.

ValueTask Limitations

Limitation 1

Never do multiple await operations on the same ValueTask object as below. The problem is after the first call object will dispose and for the next call, it might throw an exception.

ValueTask<List<Employee>> employeesTask = GetEmployees();
Var a = await employeesTask;
Var b = await employeesTask;

Limitation 2

Never do a parallel call concurrently as below

ValueTask<List<Employee>> employeesTask = GetEmployees();
Task.Run(async a => await employeesTask);
Task.Run(async b => await employeesTask);

Limitation 3

Never call GetAwaiter() on ValueTask method. As we don’t know when the method will be complete. Instead, use the IsCompleted property to check whether the task is completed or not. Once it did, we can do another call.

ValueTask<List<Employee>> employeesTask = GetEmployees();
Var a = emplyeeTask.GetAwaiter().GetResult();

Hope you got some basic understanding, of the advantages and limitations of the ValueTask type.

Happy Coding :)