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:
- An
async
function that returns aTask
can complete either synchronously, or asynchronously depending on the conditions of execution - and possibly depending also on the state of the underlying hardware. - 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 aTask
is after all aclass
. - 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
-
internal readonly TResult? _result;
if the operation completes synchronously. -
internal readonly object? _obj;
stores aTask<TResult>
for the asynchronous case -
internal readonly object? _obj;
can also store anIValueTaskSource<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.