In this post, I will be mentioning about ValueTask
and ValueTask<TResult>
classes, which represent asynchronous operations like other task based classes such as Task
and Task<TResult>
.
We will be seeing its uses and benefits in the next sections. We will first see an example asynchronous method, inspect it and talk about its possible problems. Then, we will be talking about what we can do to overcome those problems. After that, to see the effect of our changes we will compare the benchmark results of both approaches and assess the results.
Task (A simple asynchronous method)
Task
class is a representation of an asynchronous operation. It’s a “promise” and represents the eventual completion of some operation. It basically provides a mechanism to manage the state (faulted, canceled, completed, etc.) of code execution that will complete in the future.
With C# 5, async
and await
keywords came into our lives and make it easy to implement asynchronous methods by following the TAP (Task-based asynchronous pattern).
An asynchronous method that is based on TAP, can be seen as follows:
public async Task<int> GetTotalCountAsync()
{
if(_countCache == 0)
{
_countCache = await _postRepository.GetTotalCountAsync();
}
return _countCache;
}
Calling the asynchronous method:
var count = await GetTotalCountAsync();
When we create such a method, we can call it by using the await
keyword as can be seen in the example, and prevent blocking the current thread. This makes our application responsive and prevents our app to be frozen. It allows users to perform other operations in our application while waiting for the asynchronous application has been completed. For example, in a todo application, while waiting to load todo list, you can still take notes, so the UI thread has not been blocked.
This is how we can easily create and use an asynchronous method in C# without worrying about threads that created (doesn’t have to create new threads, it could use the current thread according to the current async operation state, or other aspects) and managed.
However, let’s inspect our method flow again and see if the framework offers us a better solution that we can use:
public async Task<int> GetTotalCountAsync()
{
if(_countCache == 0)
{
_countCache = await _postRepository.GetTotalCountAsync();
}
return _countCache;
}
- When we inspect our code again, we can see that our method can be completed synchronously if the total count has been already cached and passed to the
_countCache
variable. In that case, it will not be executed asynchronously and will just return the value. - Actually, most of the time this method will be executed synchronously as you might guess. It’s the hot path of our code. (The hot path is a section of our code that executes frequently)
- Nevertheless, a
Task<TResult>
object will be created on the heap memory even if it just needs to return a single value (because theTask
object will be wrapped in the value) and this will be a burden on GC (Garbage Collector.) - This can become a problem when instances of
Task
objects are created a lot in methods where high-throughput and performance is a primary concerns. - As a result, we know there is no need to create an instance of a
Task
object (if a method can be run synchronously - if it has a hot path). This lead us to find a solution that don’t allocate in heap memory and if the method is just returning a value, directly returns it. In that point,ValueTask<TResult>
comes into play.
ValueTask
ValueTask
was introduced in .NET Core 2.0 as a struct capable of wrapping either a TResult
or a Task<TResult>
. It’s a value-type, which is stored in the stack memory instead of the heap memory and as you may know CPU is responsible for managing the stack memory and clearing it periodically. So, this means GC will not be working on it to dispose of it. Returning just the value will result in fewer allocations in memory which will improve not only the memory consumption of the method but the executing performance as well.
Let’s update our method and make its return type as ValueTask<int>
:
public async ValueTask<int> GetTotalCountAsValueTaskAsync()
{
if(_countCache == 0)
{
_countCache = await _postRepository.GetTotalCountAsync();
}
return _countCache;
}
That’s it. We have just changed the method type Task<int>
to ValueTask<int>
. This gives us the following advantages:
- Fewer allocations. (When the method is
ValueTask
, aTask
object will be created when we do an asynchronous call and in the case of a synchronous call, it will directly return the result object without creating aTask
object) - Better performance in most cases. (After the memory allocations are reduced, it will give us more space in memory to perform operations)
- More awareness of the code. (By inspecting the hot paths in a method and concerning to use
ValueTask
class, will give us more awareness on code implementation and usage).
ValueTask
is suitable for an asynchronous operation that involves synchronous hot paths.
Let’s list some of limitations of the ValueTask
:
- Should not use await keyword on the same
ValueTask
object. (The problem here, after the first call it might be disposed and can be deleted from the stack memory, therefore it might throw an exception. If you need to await the sameValueTask
object multiple times, you should re-consider it or use theAsTask
extension method to obtain aTask
object) - It can’t be accessed from multiple threads concurrently. For such use cases,
Task
should be used. - Using
ValueTask
introduces additional overhead.
In conclusion, we should use ValueTask
only if it gives us significant performance gains and if performance and memory allocation is our primary concern. In general use, Task
provides more flexibility.
Benchmark Results
So far, we have seen the both Task
and ValueTask
types and their use cases. Now, let’s see the benchmark results of our methods and compare them.
I created a console application and installed the BenchmarkDotNet
package to the project and after that, I executed our methods 10000000 times in a loop to see the real effect when this methods called multiple times. The results of the benchmarks will be logged to the console, when we run the application. So, let’s run the application and compare the results:
As can be seen in the image, the method that returns Task<int>
is allocated almost twice the memory as the ValueTask<int>
method. This can be a problem if the related method is called a lot. As a result, using the ValueTask<int>
as the return type of method is more performant and allocates fewer bytes.
Conclusion
In this post, we have seen the Task
and ValueTask
types and their pros/cons to each other. As a summary:
Task
as a class is very flexible and has great benefits. For example, you can await it multiple times, from any number of consumers concurrently. You can use it without the need to worry about it. It only has a potential downside, where instances are created a lot and where high-throughput and performance are primary concerns. Because, it’s class and any operation which needs to create and allocate an object, and the more objects that are allocated, the more work the GC needs to do, and the more resources we spend on it that could be spent doing other things.ValueTask
only should be used if there is a sync hot-path in methods and significant performance gains are needed.
Most of the time, it’s better to go with Task
class. If you had bottlenecks, then you can change the return type to ValueTask
.
Thanks for reading the post, see you in the next one.