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

December 12, 2024 Dipankar Haldar 28 people viewed this post

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 Task overhead 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

  1. Sync (Thread.Sleep) completely blocks the thread, preventing any other execution.
  2. Async (Task.Delay) allows non-blocking execution, making it optimal for I/O-bound tasks.
  3. ValueTask reduces allocation overhead, making it useful for performance-sensitive applications.
  4. 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

  1. Use async for I/O-bound operations (e.g., file access, web requests).
  2. Avoid async void—use Task instead for exception handling.
  3. Prefer ValueTask<T> when frequent lightweight tasks are used.
  4. Use Parallel.ForEachAsync only for CPU-bound parallel processing, not for I/O-bound delays.
  5. Avoid mixing async and sync to prevent deadlocks.

Conclusion

For different workloads in .NET 9:

  • Use async (Task) for I/O-bound operations.
  • Use ValueTask when reducing allocation overhead is critical.
  • Use Parallel.ForEachAsync only for CPU-bound workloads that require concurrency.
  • Use sync only when necessary to avoid thread blocking.