Hello there
My current technology stack: .NET 9, Python, TypeScript, and Azure.
I develop microservices and terraform of different sizes. Sharing my challenges and key learning.
About
The views expressed in this blog are my own and do not reflect my employer's. I am not responsible for any consequences of using the information provided. This blog is for educational purposes only, not for commercial use. Readers should apply their own judgment.
Async vs Sync vs ValueTask vs Parallel.ForEachAsync in .NET 9
Async vs Sync vs ValueTask vs Parallel.ForEachAsync in .NET 9: Deep Dive & Performance Analysis
.NET 9 introduces various optimizations for asynchronous programming, making it essential to compare different execution models. In this blog, we will analyze and benchmark sync, async (Task), ValueTask, and Parallel.ForEachAsync to determine the best approach for different workloads.
Why Compare Async, Sync, ValueTask, and Parallel Processing?
- Synchronous execution (
sync): Simple but blocks threads, leading to potential performance bottlenecks. - Asynchronous execution (
async/await): Non-blocking, allowing better responsiveness but with memory overhead. - ValueTask: Optimized for low allocation async calls, reducing
Taskoverhead in performance-sensitive scenarios. - Parallel.ForEachAsync: Optimized for CPU-bound workloads, enabling efficient parallel execution.
Key Performance Considerations
| Feature | Sync (Blocking Call) |
Async (await Task) |
ValueTask | Parallel.ForEachAsync |
|---|---|---|---|---|
| CPU Usage | Higher (if blocked) | Lower (if I/O-bound) | Lower | Optimized for CPU-bound workloads |
| Thread Blocking | Blocks current thread | Non-blocking | Non-blocking | Efficient task distribution |
| Scalability | Lower | High (best for I/O) | High | High (best for CPU workloads) |
| Complexity | Simpler | Higher (callback handling) | Higher | Moderate (requires tuning) |
| Memory Usage | Lower | Higher (Task overhead) |
Lower | Depends on workload |
Benchmark Code
public class AsyncVsSyncBenchmark
{
private const int DelayMs = 1000;
private readonly List<int> numbers = new() { 1, 2, 3, 4, 5 };
public void SyncMethod()
{
Thread.Sleep(DelayMs);
}
public async Task AsyncMethod()
{
await Task.Delay(DelayMs);
}
public async ValueTask ValueTaskMethod()
{
await Task.Delay(DelayMs);
}
public async Task ParallelForEachAsync()
{
await Parallel.ForEachAsync(numbers, async (num, _) =>
{
await Task.Delay(DelayMs);
});
}
}
Test Environment
- Processor: 13th Gen Intel Core i7-1370P, 1 CPU, 20 logical and 14 physical cores
- .NET SDK: 9.0.103
- Runtime: .NET 9.0.2 (9.0.225.6610), X64 RyuJIT AVX2
Benchmark Results
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| SyncMethod | 1.008 s | 0.0071 s | 0.0067 s | 400 B |
| AsyncMethod | 1.005 s | 0.0059 s | 0.0055 s | 1016 B |
| ValueTaskMethod | 1.004 s | 0.0073 s | 0.0069 s | 1576 B |
| ParallelForEachAsync | 1.007 s | 0.0052 s | 0.0049 s | 3800 B |
Analysis & Key Takeaways
- Sync (
Thread.Sleep) completely blocks the thread, preventing any other execution. - Async (
Task.Delay) allows non-blocking execution, making it optimal for I/O-bound tasks. - ValueTask reduces allocation overhead, making it useful for performance-sensitive applications.
- Parallel.ForEachAsync does not improve performance in this case, as the workload is I/O-bound and each task is still delayed for 1 second.
Best Practices
- Use
asyncfor I/O-bound operations (e.g., file access, web requests). - Avoid
async void—useTaskinstead for exception handling. - Prefer
ValueTask<T>when frequent lightweight tasks are used. - Use
Parallel.ForEachAsynconly for CPU-bound parallel processing, not for I/O-bound delays. - Avoid mixing
asyncandsyncto prevent deadlocks.
Conclusion
For different workloads in .NET 9:
- Use
async(Task) for I/O-bound operations. - Use
ValueTaskwhen reducing allocation overhead is critical. - Use
Parallel.ForEachAsynconly for CPU-bound workloads that require concurrency. - Use
synconly when necessary to avoid thread blocking.