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.
{
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.
{
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.
{
await PerformOperationAsync();
}
4. Using IAsyncDisposable
For asynchronous cleanup, implement IAsyncDisposable and use await using to ensure resources are released properly.
{
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
{
Task task1 = DoTask1Async();
Task task2 = DoTask2Async();
await Task.WhenAll(task1, task2);
}
6. Cancellation Support
Support cancellation in asynchronous methods using 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.
{
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.
{
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.
10. Custom Task Schedulers
For advanced scenarios, like limiting concurrency or capturing synchronization contexts, consider implementing a custom 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.
{
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.
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.
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.
{
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.
{
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.
{
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.
{
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 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.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.
{
await StartApplicationAsync();
}
24. Optimize Async Loops
For loops performing asynchronous operations, consider batching or parallelizing tasks to improve throughput.
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