Introduction
In Part 1 of the “Asynchronous programming with async and await in C#” series, we examined some of the theory behind async / await. In Part 2, we discussed about how async / await pattern looks under the hood and we demystified the concepts behind it. In today’s Part 3, we will take a closer look at the different async method return types.
Async return types
The foundation of async programming lies in the various return types associated with async methods. Let’s navigate through the possibilities:
Task<TResult>
Used for async methods that return a value, Task<TResult> allows you to yield a result upon completion. This is particularly handy when your asynchronous operation needs to produce a tangible output.
Task
For async methods performing operations without returning a value, the Task return type is your go-to choice. Think of it as executing a task asynchronously without expecting a specific result.
void
In event handlers, where no specific result is expected, the void return type comes into play. However, using void in async methods that aren’t event handlers may lead to challenges, as the method cannot be awaited, and unhandled exceptions might disrupt your application.
Any type that has an accessible GetAwaiter method
Starting with C# 7.0, any type with an accessible GetAwaiter method can be used for asynchronous operations. For example, the .NET Framework supports ValueTask<TResult> or ValueTask, providing a performance boost by using structs instead of classes (more on that later).
IAsyncEnumerable<TResult>
Introduced in C# 8.0, IAsyncEnumerable<TResult> allows async methods to return an asynchronous stream of values. This is particularly useful when dealing with elements generated in chunks through repeated asynchronous calls.
Task<TResult> & Task return types
Diving deeper, we explore the distinctions between Task<TResult> and Task return types:
Task<TResult>
Used when an async method includes a return statement with an operand of type TResult. This type allows you to explicitly return a value from your asynchronous method.
async Task<string> ShowTodaysInfoAsync()
{
var ret = $"Today is {DateTime.Today:D}\n" +
"Today's hours of leisure: " +
$"{await GetLeisureHoursAsync()}";
return ret;
}
async Task<int> GetLeisureHoursAsync()
{
// Task.FromResult is a placeholder for actual work that returns a string.
var today = await Task.FromResult(DateTime.Now.DayOfWeek.ToString());
// The method then can process the result in some way.
int leisureHours;
if (today.First() == 'S')
{
leisureHours = 16;
}
else
{
leisureHours = 8;
}
return leisureHours;
}
Task
Async methods without a return statement or with a return statement lacking an operand typically use the Task return type. In such cases, the emphasis is on performing a task asynchronously without the need for a specific result.
async Task DisplayCurrentInfoAsync()
{
await WaitAndApologizeAsync();
Console.WriteLine($"Today is {DateTime.Now:D}");
Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
Console.WriteLine("The current temperature is 76 degrees.");
}
async Task WaitAndApologizeAsync()
{
// Task.Delay is a placeholder for actual work.
await Task.Delay(2000);
// Task.Delay delays the following line by two seconds.
Console.WriteLine("\nSorry for the delay. . . .\n");
}
When to use a Task and when a Task<TResult>
Choosing between Task and Task<TResult> in an async method mirrors the decision between void and any other type in a synchronous method. Task<TResult> comes into play when you need to return information, while Task is suitable when no specific return value is expected.
The Task<TResult> class has a Result property of type TResult that contains whatever you pass back with return statements in the method. The caller generally retrieves what’s stored in the Result property implicitly, by using await.
You can also access the .Result property of a Task explicitly, but we should always try to avoid it. The Result property is a blocking property, so if you try to access it before its task is finished, the thread that’s currently active is blocked until the task completes and the value is available.
void return type
You generally are advised to use the void return type only in asynchronous event handlers, which require a void return type. For all other methods that don’t return a value, you should return a Task instead, because an async method that returns void can’t be awaited. Any caller of such a method must continue to completion without waiting for the called async method to finish, just like a “fire-and-forget” like operation.
Furthermore, the caller of a void-returning async method can’t catch exceptions thrown from the method, so such unhandled exceptions are likely to cause your application to fail. On the contrary, if a method that returns a Task or Task<TResult> throws an exception, the exception is stored in the returned task, and is then rethrown when the task is awaited. For this reason, we should make sure that any async method that can produce an exception has a return type of Task or Task<TResult> and that calls to the method are awaited.
Generalized async return types and ValueTask<TResult>
Because Task and Task<TResult> are reference types, memory allocation in performance-critical paths, particularly when allocations occur in tight loops, can adversely affect performance.
To address performance concerns related to memory allocation, .NET introduces generalized return types, including the ValueTask<TResult> structure. This lightweight value type can be used instead of reference types, providing potential performance improvements.
When to use ValueTask instead of Task
ValueTask and ValueTask<TResult> are defined as structs, contrasting with the class-based Task and Task<TResult>. A class instance is a reference to data that lives in long-term memory, also called the Heap, and thus it must be allocated. A struct is a value type whose data lives in short-term memory, also called the Stack, and does not require allocation. Since long-term memory allocation can be expensive, using a ValueTask can sometimes yield better performance.
Does this mean that we should always utilize ValueTask instead of Task? It turns out, it is not that simple. Keep in mind that a ValueTask wraps a Task, so when the ValueTask’s Task field is populated, not only does it still involve long-term memory, but it also uses more memory overall than a Task by itself.
ValueTask source code
As a general recommendation, we should always use Task or Task<TResult> and only consider using ValueTask or ValueTask<TResult> if profiling our code with a performance analysis tool indicates that the allocations associated with Task are a bottleneck for our application. For example, if our code was structured asynchronously, but most of the time executes and returns from the synchronous part of the code, like we can observe in the following code sample. In the this example, the asynchronous part of the code gets executed only when we get a “lucky 6” after every rolling of the dice:
static Random? randomNumberGenerator;
async ValueTask<int> GetDiceRollAsync()
{
Console.WriteLine("...Shaking the dices...");
var roll1 = await RollAsync();
var roll2 = await RollAsync();
return roll1 + roll2;
}
async ValueTask<int> RollAsync()
{
randomNumberGenerator ??= new Random();
var diceRoll = randomNumberGenerator.Next(1, 7);
if (diceRoll == 6)
{
// Task.Delay is a placeholder for actual work
// that needs to be done asynchronously whenever we
// get a lucky 6!
await Task.Delay(5000);
}
return diceRoll;
}
Async streams with IAsyncEnumerable<TResult>
C# 8.0 introduces a fascinating feature — async streams represented by IAsyncEnumerable<TResult>. This enables async methods to return streams of asynchronous values, facilitating efficient enumeration of elements generated in chunks through repeated asynchronous calls.
You can find the code for the above examples here.
Conclusion
Mastering the intricacies of async method return types is pivotal for harnessing the full potential of asynchronous programming in C#. By understanding when to use Task, Task<TResult>, or even ValueTask<TResult>, you can craft efficient, responsive, and robust applications. Stay tuned for more insights and practical tips in our ongoing exploration of Asynchronous Programming with async and await in C#.