How to Improve aync await Performance

Improving the performance of async/await in C# involves several best practices and optimizations. Here are some key techniques to enhance performance:

1. Use Asynchronous APIs Efficiently

Avoid using Task.Run unnecessarily for IO-bound operations. Instead, prefer truly asynchronous methods provided by the framework.

✅ Example (Good)

public async Task<string> ReadFileAsync(string filePath)
{
    return await File.ReadAllTextAsync(filePath);
}

❌ Example (Bad)

public async Task<string> ReadFileAsync(string filePath)
{
    return await Task.Run(() => File.ReadAllText(filePath)); // Unnecessary Task.Run
}

2. Avoid Blocking Calls (.Result, .Wait(), .GetAwaiter().GetResult())

Blocking calls can cause deadlocks and degrade performance.

❌ Bad Practice

public void ProcessData()
{
    var data = GetDataAsync().Result; // Blocks the calling thread
}

✅ Better Approach

public async Task ProcessDataAsync()
{
    var data = await GetDataAsync();
}

3. Use ConfigureAwait(false) Where Appropriate

By default, await captures the synchronization context, which can slow down performance in UI or ASP.NET applications. If you don't need to update the UI or the request context, use .ConfigureAwait(false).

✅ Example

public async Task<string> FetchDataAsync()
{
    using var client = new HttpClient();
    return await client.GetStringAsync("https://example.com").ConfigureAwait(false);
}

⚠️ Note. In ASP.NET Core, ConfigureAwait(false) is usually not needed, but in desktop apps (WinForms/WPF), it can be useful.

4. Use ValueTask<T> for Short-Lived Tasks

Task<T> involves heap allocations, while ValueTask<T> avoids extra allocations when the task is already completed.

✅ Example

public ValueTask<int> GetNumberAsync(bool isCached)
{
    return isCached ? new ValueTask<int>(42) : ComputeNumberAsync();
}

private async Task<int> ComputeNumberAsync()
{
    await Task.Delay(100); // Simulate async work
    return 42;
}

Use ValueTask<T> when

  • The operation completes synchronously most of the time.
  • You need to reduce memory allocations.

5. Use Parallel.ForEachAsync for Concurrency

Instead of using Task.WhenAll in loops, Parallel.ForEachAsync provides optimized parallel execution.

✅ Example

public async Task ProcessItemsAsync(IEnumerable<int> items)
{
    await Parallel.ForEachAsync(items, async (item, token) =>
    {
        await Task.Delay(100); // Simulate work
        Console.WriteLine($"Processed: {item}");
    });
}

Why?

  • More efficient than Task.Run per iteration.
  • Automatically handles the degree of parallelism.

6. Minimize async void, Prefer async Task

Using async void leads to unhandled exceptions and makes debugging difficult.

❌ Bad

public async void OnButtonClick(object sender, EventArgs e)
{
    await SomeAsyncOperation();
}

✅ Better

public async Task OnButtonClickAsync(object sender, EventArgs e)
{
    await SomeAsyncOperation();
}

Exception Handling

If you must use async void, wrap it in a try-catch block.

7. Use Task.WhenAll for Multiple Asynchronous Calls

Instead of awaiting tasks sequentially, use Task.WhenAll to run them concurrently.

❌ Bad (Sequential Execution)

public async Task ProcessDataAsync()
{
    var result1 = await GetDataAsync();
    var result2 = await GetMoreDataAsync();
}

✅ Good (Concurrent Execution)

public async Task ProcessDataAsync()
{
    var task1 = GetDataAsync();
    var task2 = GetMoreDataAsync();
    var results = await Task.WhenAll(task1, task2);
}

Why?

Reduces total execution time by running both tasks simultaneously.

8. Optimize Long-Running Operations with Task.Run

For CPU-bound work, offload computations to background threads.

✅ Example

public async Task<int> ComputeHeavyAsync()
{
    return await Task.Run(() =>
    {
        Thread.Sleep(5000); // Simulating heavy computation
        return 42;
    });
}

When to use?

When the task is CPU-intensive (e.g., image processing, encryption).    

9. Use Cancellation Tokens

Always allow operations to be canceled to avoid wasted resources.

✅ Example

public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
    using var client = new HttpClient();
    var response = await client.GetAsync("https://example.com", cancellationToken);
}

Why?

Helps free up resources if the user cancels the operation.

10. Profile and Benchmark with Diagnostic Tools

  • Use dotnet-trace to analyze performance.
  • Use BenchmarkDotNet for micro-benchmarking.
  • Use Visual Studio Profiler to analyze async call stacks.

✅ Example Benchmark

[Benchmark]
public async Task MeasureAsyncOperation()
{
    await FetchDataAsync();
}

Summary

  • Use Native Async APIs: Avoid unnecessary Task.Run for IO-bound operations.
  • Avoid Blocking Calls: Don't use .Result, .Wait(), or .GetAwaiter().GetResult().
  • Use ConfigureAwait(false): Prevents unnecessary context switches in non-UI apps.
  • Leverage ValueTask<T>: Reduces heap allocations for frequently completed tasks.
  • Use Parallel.ForEachAsync: Executes tasks efficiently in parallel.
  • Avoid async void: Use async Task for better exception handling.
  • Use Task.WhenAll: Run multiple async tasks concurrently instead of sequentially.
  • Offload CPU-bound Work with Task.Run: Prevents blocking the main thread.
  • Use Cancellation Tokens: Allows safe task cancellation to free up resources.
  • Profile & Benchmark: Use dotnet-trace, BenchmarkDotNet, and profilers to find bottlenecks.

Would you like more details on any specific point? 

Up Next
    Ebook Download
    View all
    Learn
    View all