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

ssukhpinder

Sukhpinder Singh

Posted on April 4, 2024

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

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;
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

6. Cancellation Support

Support cancellation in asynchronous methods using CancellationToken.

    public async Task DoOperationAsync(CancellationToken cancellationToken)
    {
        await LongRunningOperationAsync(cancellationToken);
        cancellationToken.ThrowIfCancellationRequested();
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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.
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
ssukhpinder
Sukhpinder Singh

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