Async/Await

Introduction

Async should typically be used for I/O bound operations e.g. when reading from a file, sending a request via the internet etc

  • A program is CPU bound if it would go faster if the CPU was faster, i.e. it spends the majority of its time simply using the CPU (doing calculations). A program that computes new digits of π will typically be CPU-bound, it's just crunching numbers.
  • A program is I/O bound if it would go faster if the I/O subsystem was faster. Which exact I/O system is meant can vary; You typically associate it with disk, but of course networking or communication in general is common too. A program that looks through a huge file for some data might become I/O bound, since the bottleneck is then the reading of the data from disk.

C# 5 introduced a simplified approach to doing async programming using the async and await keywords. See https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ for more information.

When doing I/O bound work, you don't want to consume threads of your server's thread pool because this work doesn't actually involve the CPU. Executing such operations on a thread means this thread would just sit idle and wait until the I/O operations are finished.

  • To avoid this, you need to delegate this work to the appropriate driver, register a callback with the I/O Completion Port, release the working thread, and resume processing on it only after the work is completed and the callback is called. This allows you to increase the application's throughput and avoid such nasty things as thread starvation (or at least delay it). This is what happens when you await an async operation. The await keyword marks a point where the method can't continue until the awaited asynchronous operation is complete. In the meantime, the method is suspended, and control returns to the method's caller.

Important Note: Async/await in ASP.NET is not about making your application perform better, it is about freeing up the server's thread pool so you can run more concurrent requests.

A quick side note about parallelism versus asynchrony. Parallel execution is a subset of asynchronous execution: every parallel execution is asynchronous, but not every asynchronous execution is parallel.

  • Executing something asynchronously corresponds to "without blocking the caller". It can be achieved by starting this work immediately, which means it will be done in parallel, or by waiting until the caller is finished with their stuff, which means the work will be executed sequentially, either by the same or another thread. Either way, the job will not block the caller and thus we can say that it will be executed asynchronously.

Example

The following will hang the browser for 5 seconds since we only await the second call to the async Sleep method. However it will free the thread from the application pool during that time. It also shows how we can call async methods without using await to execute tasks in parallel.

public async Task<IActionResult> Index() {
    Sleep(5000);

    await Sleep(5000);

    Sleep(5000);

    return View();
}

private async Task Sleep(int duration) {
    await Task.Delay(duration);
}

Note: Calling an async method without awaiting the result acts as a fire and forget since we won’t be able to access the result as we never wait for the result to return. See Task.ContinueWith below on how we can do this and still handle exceptions.

Database - NHibernate

Database communication is an I/O bound operation as your application talks to it via the server's network adapter, so it's a good idea to use async operations for it as well.

Here's an example of retrieving a single record:

var customer = await session.GetAsync<Customer>(1);

Here's an example of retrieving a list of records:

var customers = await session.Query<Customer>().ToListAsync();

This feature is not intended for parallelism, only non-parallel asynchrony, so make sure to await each call before issuing a new call using the same session. If you forget to await each call then you may run into issues where the same session is accessing the database at the same time on two concurrent threads. This will throw an exception. In other words, don't do this:

var task1 = session.GetAsync<Customer>(1);
var task2 = session.GetAsync<Customer>(2);

var customers = await Task.WhenAll(task1, task2);

Do this instead:

var customer1 = await session.GetAsync<Customer>(1);
var customer2 = await session.GetAsync<Customer>(2);

See https://enterprisecraftsmanship.com/2017/12/11/nhibernate-async-support/ for more information.

LINQ

Please refer to the following https://stackoverflow.com/a/66331594/155899. We now take a dependency on “System.Linq.Async”, therefore it is recommended to use the alternative solution in the update. As we mentioned above Task.WhenAll is not the solution as we will run into concurrency issues.

Advanced

Task.ConfigureAwait(false) – In ASP.NET if you was to call an async method synchronously (without awaiting) and then call the Result property of the Task returned then you would get stuck in a deadlock. For example:

public async Task<IActionResult> Index() {
    var s = Sleep(5000);
    var r = s.Result;

    return View();
}

private async Task<bool> Sleep(int duration) {
    await Task.Delay(duration);

    return true;
}

The solution is to add ConfigureAwait(false) after Task.Delay. However in ASP.NET Core this is no longer needed and you no longer get a deadlock so it Is now advised not to use ConfigureAwait(false).

Task.Run – This can be used to run code asynchronously when you are in a synchronous method. However you also won’t be able to access the result. This known as fire and forget. When you are in an async method you don’t need to do this as you simply have to call the async method without awaiting the result.

Task.ContinueWith – This can be used to call an async method without awaiting the result. However this time you will be able to handle any exceptions. For example:

public async Task<IActionResult> Index() {
    _ = Sleep(5000).ContinueWith(t => {
        // Log the exception here
        Console.WriteLine(t.Exception);
    }, TaskContinuationOptions.OnlyOnFaulted);

    return View();
}

private async Task<bool> Sleep(int duration) {
    await Task.Delay(duration);

    throw new Exception();

    return true;
}

This could possibly be moved to an extension method to make its intentions clearer e.g.

public static void PerformAsyncTaskWithoutAwait(this Task task, Action<Task> exceptionHandler) {
    _ = task.ContinueWith(t => exceptionHandler(t), TaskContinuationOptions.OnlyOnFaulted);
}

We need to make sure the calling context is the same in the exception handler. Also is there a possibility of accessing the same session at the same time on two concurrent threads if two tasks running at the same time both throw an exception?

Can you emit the async/await keywords?

If you return a task and it is the only call which is awaited within the method you can shorten the following:

public async Task<IUser> GetUserAsync(string userName) {
    return await GetUserAsync<IUser>(userName);
}

To:

public Task<IUser> GetUserAsync(string userName) {
    return GetUserAsync<IUser>(userName);
}

However although this gives you a slight performance improvement there are multiple cases where this could lead to problems and therefore it is recommended to always include the async/await keywords. See https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/0ba7625050f975f8a7df1df57c80ad08da250541/AsyncGuidance.md#prefer-asyncawait-over-directly-returning-task for more information.

Further Reading