One of the number one features to arrive in C# 7 for me is the addition of the ValueTask to our asynchronous tool box. But, before we look at some code showing ValueTask in action, let’s first address the problem it solves.
To demonstrate, I’ve put together an experiment which, in a small way, shows a visible difference between the problem and cure. I’ll be using a standard .NET console application which will require the following the nugget package…
System.Threading.Tasks.Extensions
I’m also using Visual Studio 2017.
The Problem
Take a look at the following code. Then, we’ll identify where the problem lies…
static void Main(string[] args) { var data = GetDataFromTask(); Console.WriteLine(data.Count()); Console.WriteLine("complete"); Console.ReadLine(); } static IEnumerable<int> GetDataFromTask() { Console.WriteLine("Data From Task"); for (int i = 0; i < 1000000000; i ++) { yield return Task.FromResult(i * 10).Result; } }
Now, whatever our GetDataFromTask method may do, we want to return a value type—in this case, an int—from the Task.FromResult within; this presents the problem. The value type is being wrapped in a Task which, of course, is a reference type, and, therefore will be treated accordingly by memory management.
In loops like we just saw, especially when the task is being run synchronously, an impact to performance could be observed. If the amount of data being stored in memory is considerable, you also may see further performance issues when the garbage collector moves to complete its process.
Let’s run the preceding code in debug mode, and we observe results that look something like what we see in Figure 1.
Figure 1: Our code running, with a Task returned
In Figure 1, we can see that garbage collection did indeed occur once at around 10 seconds into the run, designated by the yellow marker, and there was just below 14MB of process memory in use. Furthermore, the loop took just short of 20 seconds to complete.
Using ValueTask
Do note that this example uses code that doesn’t reflect what you would find in the real world. But, let’s now go ahead and add the code from the following…
static IEnumerable<int> GetDataFromValueTask() { Console.WriteLine("Data From ValueTask"); for (int i = 0; i < 1000000000; i++) { yield return new ValueTask<int>(i * 10).Result; } }
Instead of using Task.FromResult, we’ll now use ValueTask<int>; and compute the result as we did previously. Now, let’s run this code, and observe any differences we may see.
Figure 2: The code running making use of ValueTask
The first observation we can make here is that the process memory in use is sitting below 11 MB, and there was no garbage collection during this run. Secondly, even through it’s only a small amount, the time taken to complete the run was shorter.
As said, we’re using code in this example that doesn’t reflect production code; but, if you were to put this in the context of long running and tightly looped async code, the possible benefits become clear. I’ve also used Task.FromResult and ValueTask<int> inline within the called methods. If you’re familiar with Task, you also can write code like what we see next.
async ValueTask<int> DoSomeWorkAsync() { // async code }
ValueTask also gives us a constructor with the parameter that takes a task. This gives us the ability to construct a ValueTask from any other async method we may have; for example:
void DoSomethingAboutThis() { var valueTask = new ValueTask<bool>(DidItWorkAsync()); } async Task<bool> DidItWorkAsync() { await Task.Delay(10); return true; }
And finally, for the sake of completeness, let’s run the code from above using a standard synchronous method. This is what we would use for such code under normal circumstances…
Figure 3: The results from running the loop using standard synchronous code
Conclusion
It took me a moment to put together a small example that was easy to follow along for this article and produce results that were visibly different. I would strongly recommend experimenting with ValueTask, and doing so before using it in production code you verify if it’s useful to you. That said, if it is useful, it can be very useful indeed!
If you have any questions on this article, you can find me on Twitter @GLanata.