top of page

Secrets of .NET Async/Await

Updated: Feb 23, 2023

Async/await is a feature of the .NET framework that allows developers to write asynchronous code in a more readable and maintainable way. With async/await, you can write code that performs time-consuming operations without blocking the main thread of execution.



Async/await is based on the Task-based Asynchronous Pattern (TAP) in .NET. In the TAP model, methods that perform asynchronous operations return a Task or Task<T> object that represents the ongoing operation. The calling code can then await the result of the task without blocking the thread.


Async/await is particularly useful when working with UI applications, where blocking the main thread can cause the application to become unresponsive. By using async/await to perform time-consuming operations, you can keep the UI thread free to respond to user input.


.NET Async/Await

While async/await in .NET is a well-known feature, there are still some secrets that developers may not be aware of. Here are some top secrets of .NET async/await:


1. Async all the way

One important aspect of async/await in .NET is the principle of "async all the way". This means that any method that calls an async method should also be marked as async, and so on down the call stack. This is important because it ensures that the entire call stack is async, and avoids blocking the main thread.


Example:

public async Task<int> GetTotalAsync()
{
    var value1 = await GetValue1Async();
    var value2 = await GetValue2Async();
    return value1 + value2;
}

private async Task<int> GetValue1Async()
{
    return await Task.FromResult(1);
}

private async Task<int> GetValue2Async()
{
    return await Task.FromResult(2);
}

In this example, GetTotalAsync() is marked as async because it calls two other async methods (GetValue1Async() and GetValue2Async()). By using await to asynchronously wait for the results of these methods, we ensure that the entire call stack is async.


2. Avoid deadlocks

Deadlocks can occur when a method waits for a task to complete, but that task itself is waiting for the same method to complete. To avoid deadlocks, you can use ConfigureAwait(false) to indicate that a task does not need to continue executing on the same context that it was started on.


Example:

public async Task<int> GetValueAsync()
{
    return await Task.Run(() =>
    {
        // Perform some CPU-bound workreturn 42;
    }).ConfigureAwait(false);
}

In this example, ConfigureAwait(false) is used to indicate that the continuation of the task does not need to execute on the same context as the calling method. This can help to avoid deadlocks when a method is waiting for a task to complete, but the task is also waiting for the method to complete.


3. Use ValueTask for high-performance scenarios

In cases where you need to return a value from an async method, you can use ValueTask<T> instead of Task<T> to improve performance. ValueTask<T> is a lightweight structure that can avoid the overhead of allocating a new object for every task.


Example:

public async ValueTask<int> GetValueAsync()
{
    return await ComputeValueAsync().ConfigureAwait(false);
}

private async Task<int> ComputeValueAsync()
{
    // Perform some CPU-bound workawait Task.Delay(1000);
    return 42;
}

In this example, ValueTask<int> is used instead of Task<int> to avoid the overhead of allocating a new object for every task. By using ConfigureAwait(false) to avoid capturing the current context, we can further improve performance.


4. Use CancellationToken to cancel async operations

CancellationToken is a powerful feature of async/await that allows you to cancel long-running operations. You can pass a CancellationToken to an async method and periodically check whether it has been cancelled, allowing the operation to be cancelled if necessary.


Example:

public async Task<int> GetValueAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        // Perform some workawait Task.Delay(1000);
    }
    return 42;
}

In this example, a CancellationToken is passed to the async method so that it can be cancelled if necessary. The method periodically checks whether the CancellationToken has been cancelled, and returns a value if it has not.


5. Use TaskCompletionSource for custom async operations

TaskCompletionSource is a class in .NET that allows you to create custom async operations that do not rely on existing async methods. You can create a TaskCompletionSource, set its Result or Exception properties, and then use the Task that it returns to await the completion of the operation.


Example:

public Task<int> GetValueAsync()
{
    var tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        // Perform some work
        tcs.SetResult(42);
    });
    return tcs.Task;
}

In this example, TaskCompletionSource is used to create a custom async operation that does not rely on existing async methods. The Task.Run() method is used to perform some work on a background thread, and tcs.SetResult(42) is used to indicate that the operation has completed successfully.


6. Avoid mixing async and synchronous code

While it is possible to mix async and synchronous code in .NET, it can lead to performance issues and deadlocks. It is generally best to avoid mixing sync and async code, and instead convert synchronous code to async using Task.Run() or other methods.


Example:

public async Task<int> GetValueAsync()
{
    var value1 = await GetValue1Async();
    var value2 = GetValue2();
    return value1 + value2;
}

private async Task<int> GetValue1Async()
{
    return await Task.FromResult(1);
}

private int GetValue2()
{
    // Perform some synchronous workreturn 2;
}

In this example, GetValue2() is a synchronous method that is called from an async method (GetValueAsync()). While this code will compile and run, it can lead to performance issues and deadlocks. It is generally best to avoid mixing sync and async code, and instead convert synchronous code


Conclusion:

Async/await in .NET is a feature that can greatly improve the performance and responsiveness of applications. By understanding these top secrets, developers can use async/await effectively and avoid common pitfalls.

0 comments

Comments


bottom of page