24 Essential Async/Await Best Practices for Basic to Advanced C# Developers
Sukhpinder Singh
Posted on April 4, 2024
Async/await in C# is a framework used for writing asynchronous C# code that is both readable and maintainable. These tips will help you to integrate async/await programming more effectively in the # projects:
1. ValueTask for Lightweight Operations
Use ValueTask instead of Task for asynchronous methods that often complete synchronously, reducing the allocation overhead.
public async ValueTask<int> GetResultAsync()
{
if (cachedResult != null)
return cachedResult;
int result = await ComputeResultAsync();
cachedResult = result;
return result;
}
2. ConfigureAwait for Library Code
Use ConfigureAwait(false) in library code to avoid deadlocks by not capturing the synchronization context.
public async Task SomeLibraryMethodAsync()
{
await SomeAsyncOperation().ConfigureAwait(false);
}
3. Avoiding async void
Prefer async Task over async void except for event handlers, as async void can lead to unhandled exceptions and is harder to test.
public async Task EventHandlerAsync(object sender, EventArgs e)
{
await PerformOperationAsync();
}
4. Using IAsyncDisposable
For asynchronous cleanup, implement IAsyncDisposable and use await using to ensure resources are released properly.
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
}
private async ValueTask DisposeAsyncCore()
{
if (resource != null)
{
await resource.DisposeAsync();
}
}
5. Efficiently Combine Tasks
Use Task.WhenAll for running multiple tasks in parallel and waiting for all to complete, which is more efficient than awaiting each task sequentially
public async Task ProcessTasksAsync()
{
Task task1 = DoTask1Async();
Task task2 = DoTask2Async();
await Task.WhenAll(task1, task2);
}
6. Cancellation Support
Support cancellation in asynchronous methods using CancellationToken.
public async Task DoOperationAsync(CancellationToken cancellationToken)
{
await LongRunningOperationAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
7. State Machine Optimization
For performance-critical code, consider structuring your async methods to minimize the creation of state machines by separating synchronous and asynchronous paths.
public async Task<int> FastPathAsync()
{
if (TryGetCachedResult(out int result))
{
return result;
}
return await ComputeResultAsync();
}
8. Avoid Blocking Calls
Avoid blocking on async code with .Result or .Wait(). Instead, use asynchronous waiting through the stack.
public async Task WrapperMethodAsync()
{
int result = await GetResultAsync();
}
9. Eliding Async/Await
In simple passthrough scenarios or when returning a task directly, you can elide the async and await keywords for slightly improved performance.
public Task<int> GetResultAsync() => ComputeResultAsync();
10. Custom Task Schedulers
For advanced scenarios, like limiting concurrency or capturing synchronization contexts, consider implementing a custom TaskScheduler.
public sealed class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
// Implement the scheduler logic here.
}
11. Using Asynchronous Streams
Leverage asynchronous streams with IAsyncEnumerable for processing sequences of data asynchronously, introduced in C# 8.0.
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100); // Simulate async work
yield return i;
}
}
12. Avoid Async Lambdas in Hot Paths
Async lambdas can introduce overhead. In performance-critical paths, consider refactoring them into separate async methods.
// Before optimization
Func<Task> asyncLambda = async () => await DoWorkAsync();
// After optimization
public async Task DoWorkMethodAsync()
{
await DoWorkAsync();
}
13. Use SemaphoreSlim for Async Coordination
SemaphoreSlim can be used for async coordination, such as limiting access to a resource in a thread-safe manner.
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public async Task UseResourceAsync()
{
await semaphore.WaitAsync();
try
{
// Access the resource
}
finally
{
semaphore.Release();
}
}
14. Task.Yield for UI Responsiveness
Use await Task.Yield() in UI applications to ensure the UI remains responsive by allowing other operations to process.
public async Task LoadDataAsync()
{
await Task.Yield(); // Return control to the UI thread
// Load data here
}
15. Asynchronous Lazy Initialization
Use Lazy> for asynchronous lazy initialization, ensuring the initialization logic runs only once and is thread-safe.
private readonly Lazy<Task<MyObject>> lazyObject = new Lazy<Task<MyObject>>(async () =>
{
return await InitializeAsync();
});
public async Task<MyObject> GetObjectAsync() => await lazyObject.Value;
16. Combining async and LINQ
Be cautious when combining async methods with LINQ queries; consider using asynchronous streams or explicitly unwrapping tasks when necessary.
public async Task<IEnumerable<int>> ProcessDataAsync()
{
var data = await GetDataAsync(); // Assume this returns Task<List<int>>
return data.Where(x => x > 10);
}
17. Error Handling in Async Streams
Handle errors gracefully in asynchronous streams by encapsulating the yielding loop in a try-catch block.
public async IAsyncEnumerable<int> GetNumbersWithErrorsAsync()
{
try
{
for (int i = 0; i < 10; i++)
{
if (i == 5) throw new InvalidOperationException("Test error");
yield return i;
}
}
catch (Exception ex)
{
// Handle or log the error
}
}
18. Use Parallel.ForEachAsync for Asynchronous Parallel Loops
Utilize Parallel.ForEachAsync in .NET 6 and later for running asynchronous operations in parallel, providing a more efficient way to handle CPU-bound and I/O-bound operations concurrently.
await Parallel.ForEachAsync(data, async (item, cancellationToken) =>
{
await ProcessItemAsync(item, cancellationToken);
});
22. Avoid Excessive Async/Await in High-Performance Code
In performance-critical sections, minimize the use of async/await. Instead, consider using Task.ContinueWith with caution or redesigning the workflow to reduce asynchronous calls.
Task<int> task = ComputeAsync();
task.ContinueWith(t => Process(t.Result));
23. Async Main Method
Utilize the async entry point for console applications introduced in C# 7.1 to simplify initialization code.
public static async Task Main(string[] args)
{
await StartApplicationAsync();
}
24. Optimize Async Loops
For loops performing asynchronous operations, consider batching or parallelizing tasks to improve throughput.
var tasks = new List<Task>();
for (int i = 0; i < items.Length; i++)
{
tasks.Add(ProcessItemAsync(items[i]));
}
await Task.WhenAll(tasks);
More Cheatsheets
C# Programming🚀
Thank you for being a part of the C# community! Before you leave:
Follow us: Youtube | X | LinkedIn | Dev.to
Visit our other platforms: GitHub
More content at C# Programming
Posted on April 4, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
April 4, 2024