Best practices and antipatterns for Async/Await
Max Bazhenov
Senior Full Stack Developer | 8+ Years in Software Engineering | .NET (C#), Angular, TypeScript, Azure | Enterprise Web Applications | Real-time UI, Performance Optimization
In the previous posts, we explored how async/await works, when to use them, and what benefits they provide.
In this post and the last one in the series, we will cover some essential best practices that should be followed when using async/await and introduce common pitfalls which should be avoided.
1. Avoid using .Result and .Wait() as they are blocking calls
Blocking calls like .Result and .Wait() on an async method can cause the calling thread to wait synchronously until the asynchronous operation completes. This means the calling thread is blocked and cannot perform any other work during this time. The benefit of asynchronous execution and resource distribution will be lost. It also creates a potential deadlock situation. This is because the async method is awaiting the completion of a task, while the task is awaiting the release of the calling thread, leading to a circular dependency. Always use await instead of blocking calls like .Result or .Wait(). This ensures the method behaves asynchronously and avoids locking the thread.
2. Avoid using async void
As mentioned in the previous post, the Task object represents the ongoing asynchronous operation, and manages the state of the async operation (whether it's running, completed, or faulted). When define a method with async void, it does not return a Task object that can be awaited. The method’s caller will not know when the async void method has finished executing, which leads to unpredictable behavior in the application. And also exceptions thrown inside the async void method cannot be caught in the usual way.
The exception is Event handlers. Fire and forget - when an event is raised, the handler is executed without waiting for it to complete.
3. Use ConfigureAwait(false)
When an asynchronous method is awaited, the context in which the method was called is captured. By default, this means that the continuation after the await will run in the same synchronization context. By using ConfigureAwait(false), the awaiter will not capture the synchronization context. This means that the continuation can run on any thread available in the thread pool, rather than the original context. It can lead to a performance improvement by avoid the overhead of capturing the synchronization context and allows the continuation to run on a more efficient thread from the thread pool.
4. Use CancellationToken for Long-Running Operations
The CancellationToken is used to cancel long-running asynchronous operations. It’s typically passed as a parameter to async methods and allows to cancel the operation before it completes.
领英推荐
public async Task<ValueDto[]> GetValuesAsync (string filter, CancellationToken c)
It works by providing a way to signal cancellation requests. The async method checks the IsCancellationRequested property or calls ThrowIfCancellationRequested to terminate the operation early if cancellation is requested.
5. Use Task.WhenAll for concurrent execution
We have several async calls which are not depend from each other:
await GetAppInfoAsync();
await GetUserInfoAsync();
await LoadInstructuinsAsync();
In this case, awaiting each task sequentially introduces unnecessary delays as each task is awaited independently. Task.WhenAll() allows to run multiple asynchronous operations concurrently and waits for all of them to complete. It returns a Task that completes when all the provided tasks have finished executing. It improves performance by utilizing parallel execution for independent tasks. It reduces overall runtime by waiting for all tasks to complete simultaneously, rather than one by one.
await Task.WhenAll(GetAppInfoAsync(), GetUserInfoAsync(), LoadInstructuinsAsync());
When using Task.WhenAll(), any exceptions thrown by the individual tasks are captured in an AggregateException. The exceptions can handled using a try-catch block around the await Task.WhenAll() statement.
Using Task.WhenAll() improves performance by utilizing parallel execution for independent tasks. It reduces overall runtime by waiting for all tasks to complete simultaneously, rather than one by one.
Conclusion
In this post we explored the best practices and antipatterns for async/await. By following these best practices, the efficiency and reliability of async code in .NET can be improved. Async programming is powerful, but incorrect usage can introduce deadlocks and performance issues.
In the next series of posts, we will delve into more advanced topics like concurrency and parallelism in async methods.