24 Essential Async/Await Best Practices for Basic to Advanced C# Developers

Rmag Breaking News

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

Cheat Sheets – .Net

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

Leave a Reply

Your email address will not be published. Required fields are marked *