Asynchronous Programming in C# with System.Threading.ValueTask

Tobias Andersen
5 min readAug 23, 2023

--

In the world of modern software development, asynchronous programming has become a crucial tool to ensure applications are responsive and efficient. C#, a popular programming language developed by Microsoft, provides powerful tools to help developers with asynchronous programming. One such tool is the System.Threading.ValueTask struct, introduced in C# 7.0, which offers a lightweight alternative to the traditional Task type for handling asynchronous operations. In this article, we will delve into the world of asynchronous programming in C# and explore the benefits of using ValueTask, backed an illustrative code example.

Understanding Asynchronous Programming

Asynchronous programming is a technique that allows an application to perform multiple tasks concurrently without blocking the main thread. This is particularly important for tasks that might take a significant amount of time, such as I/O-bound operations (e.g., reading/writing files, network requests) or CPU-bound operations (e.g., complex calculations). Traditional these type of “blocking operations” perform poorly when handled synchronously which leads to unresponsiveness and a poor user experience.

C# introduced the async and await keywords to simplify asynchronous programming. The async keyword marks a method as asynchronous, and the await keyword is used within an async method to asynchronously wait for the completion of a task without blocking the thread. The result is cleaner, more readable code that mimics the flow of synchronous programming, as illustrated in the below snippet.

async Task<int> GetValue()
{
return Task.FromResult(0);
}

The Role of System.Threading.ValueTask

While the traditional approach to asynchronous programming involves using the Task type, which represents an operation that can be awaited, C# 7.0 introduced the ValueTask struct as an alternative. The primary motivation behind ValueTask was to reduce the overhead associated with heap allocations for short-lived tasks, thus improving performance and memory usage in scenarios with frequent asynchronous operations.

ValueTask can hold either a result value (similar to Task<TResult>) or represent a completion signal without a result. It does so by using a combination of a Task and a bool flag, allowing it to avoid unnecessary heap allocations for trivial cases.

Benefits of Using ValueTask

  1. Performance Improvement: In scenarios where an asynchronous operation completes synchronously or very quickly, using ValueTask can eliminate the overhead of creating and scheduling a new Task instance. This can lead to performance improvements and reduced memory usage.
  2. Reduced Heap Allocations: Unlike Task, which is a reference type and often requires heap allocations, ValueTask is a value type. This makes it beneficial for scenarios where minimizing heap allocations is a priority, especially for high-performance applications.
  3. Optimal for Hot Paths: Applications often have sections of code that are executed frequently. By using ValueTask in such "hot paths," you can optimize memory usage and reduce the impact of garbage collection on the application's performance.

Code Kata

In the ever-evolving landscape of asynchronous programming, combining the power of ValueTask and IAsyncEnumerable in C# brings forth a new realm of efficiency and elegance. In this example, we embark on a journey to create a simplistic yet illustrative asynchronous streaming application. By intertwining the lightweight ValueTask struct with the versatile IAsyncEnumerable interface, we craft a seamless flow of data chunks, showcasing how modern C# features can elegantly facilitate streaming scenarios. So, let's delve into the code and discover how the fusion of these two features can redefine the way we handle asynchronous streams.

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class StreamingServer
{
static async Task Main(string[] args)
{
const int port = 1234;
var ipAddress = IPAddress.Any;
var listener = new TcpListener(ipAddress, port);

listener.Start();

Console.WriteLine($"Server listening on {ipAddress}:{port}");

while (true)
{
var client = await listener.AcceptTcpClientAsync();

_ = HandleClientAsync(client);
}
}

static async Task HandleClientAsync(TcpClient client)
{
try
{
using (NetworkStream networkStream = client.GetStream())
using (StreamReader reader = new StreamReader(networkStream, Encoding.UTF8))
using (StreamWriter writer = new StreamWriter(networkStream, Encoding.UTF8) { AutoFlush = true })
{
// Simulate streaming by sending chunks of data
for (int chunkNumber = 1; chunkNumber <= 10; chunkNumber++)
{
var chunk = GenerateDataChunk(chunkNumber);

await SendChunkAsync(writer, chunk);
await Task.Delay(1000); // Simulate delay between chunks
}
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
finally
{
client.Close();

Console.WriteLine("Client disconnected");
}
}

static async ValueTask SendChunkAsync(StreamWriter writer, string chunk)
{
await writer.WriteLineAsync(chunk);
}

static string GenerateDataChunk(int chunkNumber)
{
return $"DataChunk{chunkNumber}";
}
}

In this example, the StreamingServer class sets up a TCP server that listens for incoming connections on a specified port. When a client connects, the server simulates streaming by sending ten data chunks, each separated by a second of delay.

The SendChunkAsync method uses ValueTask to asynchronously write data to the client's stream. While this example is simplified, in a real-world scenario, you might be streaming large files or multimedia content.

Please note that this is a basic example and doesn’t handle more advanced features such as error handling, proper message framing, or handling multiple clients concurrently. In a production application, you would need to consider these aspects for robustness and scalability.

Best Practices for Using ValueTask

  1. Measure and Optimize: While ValueTask can provide performance benefits, it's essential to measure and profile your application to ensure that it indeed leads to improvements. Benchmarking can help identify hot spots where ValueTask can be particularly effective.
  2. Consider Complexity: For simple operations that are unlikely to block for a long time, using ValueTask might provide a marginal benefit. However, for more complex operations or operations that can truly benefit from parallelism, using traditional Task might be a better choice.
  3. Keep Code Readable: As with any programming technique, maintainability and readability should remain a priority. Use ValueTask where it makes sense and aligns with the code's clarity.

Conclusion

Asynchronous programming is a fundamental technique for building responsive and efficient applications. C# offers powerful tools like async and await for handling asynchronous operations. With the introduction of System.Threading.ValueTask, developers have a lightweight alternative to Task that can lead to improved performance and reduced memory usage, especially in scenarios with frequently executed asynchronous operations. By understanding the benefits and proper usage of ValueTask, developers can optimize their code for better performance and responsiveness.

--

--