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?