Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Scalable and Performant ASP.NET Core Web APIs: Asynchronous Code

March 21, 2018
dotnet

This is another post in a series of posts on creating performant and scalable web APIs using ASP.NET Core. In this post, we’ll focus on making our code asynchronous, and hopefully making our API work more efficiently …

Up to now, in this blog series, all our API code has been synchronous - i.e. we haven’t used async / await yet. For synchronous API code, when a request is made to the API, a thread from the thread pool will handle the request. If the code makes an I/O call (like a database call) synchronously, the thread will block until the I/O call has finished. The blocked thread can’t be used for any other work, it simply does nothing and waits for the I/O task to finish. If other requests are made to our API whilst the other thread is blocked, different threads in the thread pool will be used for the other requests.

Sync Diagram

There is some overhead in using a thread - a thread consumes memory and it takes time to spin a new thread up. So, really we want our API to use as few threads as possible.

If the API was to work in an asynchronous manner, when a request is made to our API, a thread from the thread pool handles the request (as in the synchronous case). If the code makes an asynchronous I/O call, the thread will be returned to the thread pool at the start of the I/O call and then be used for other requests.

Async Diagram

So, making operations asynchronous will allow our API to work more efficiently with the ASP.NET Core thread pool. So, in theory, this should allow our API to scale better - let’s see if we can prove that …

Sync v Async Test

Let’s test the above theory with the following controller action methods. You can see that we prefix the method with “async” to make it asynchronous and prefix asynchronous I/O calls with “await”. In our example, we are using WAITFOR DELAY to simulate a database call that takes 2 seconds.`

[Route("api/syncvasync")]
public class SyncVAsyncController : Controller
{
    private readonly string _connectionString;
    public SyncVAsyncController(IConfiguration configuration)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }

    [HttpGet("sync")]
    public IActionResult SyncGet()
    {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            connection.Open();

            connection.Execute("WAITFOR DELAY '00:00:02';");
        }

        return Ok();
    }

    [HttpGet("async")]
    public async Task<IActionResult> AsyncGet()
    {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            await connection.OpenAsync();

            await connection.ExecuteAsync("WAITFOR DELAY '00:00:02';");
        }

        return Ok();
    }
}

Let’s load test the sync end point first: Sync test

Now let’s load test the async end point: Async test

So, we have better results for the async end point, but only just!

Let’s throttle the thread pool:

public class Program
{
    public static void Main(string[] args)
    {
        ...
        int processorCounter = Environment.ProcessorCount; // 8 on my PC        bool success = ThreadPool.SetMaxThreads(processorCounter, processorCounter);        ...
    }
}

Let’s also return the available threads in the thread pool in the responses:

[HttpGet("sync")]
public IActionResult SyncGet()
{
    using (SqlConnection connection = new SqlConnection(_connectionString))
    {
        connection.Open();

        connection.Execute("WAITFOR DELAY '00:00:02';");
    }

    return Ok(GetThreadInfo());}

[HttpGet("async")]
public async Task<IActionResult> AsyncGet()
{
    using (SqlConnection connection = new SqlConnection(_connectionString))
    {
        await connection.OpenAsync();

        await connection.ExecuteAsync("WAITFOR DELAY '00:00:02';");
    }

    return Ok(GetThreadInfo());}

private dynamic GetThreadInfo(){    int availableWorkerThreads;    int availableAsyncIOThreads;    ThreadPool.GetAvailableThreads(out availableWorkerThreads, out availableAsyncIOThreads);    return new { AvailableAsyncIOThreads = availableAsyncIOThreads, AvailableWorkerThreads = availableWorkerThreads };}

Let’s load test the sync end point again now that the thread pool has been throttled:

Sync load test 2

We can see from the responses that all the threads in thread pool are being used:

Sync load test 2

Load testing the async end point again shows it is more efficient than the sync end point:

Sync load test 2

We can see from the responses that not all the threads in thread pool are being used - the thread pool is being used more efficiently:

Sync load test 2

Conclusion

When the API is stressed, async action methods will give the API some much needed breathing room whereas sync action methods will deteriorate quicker.

Async code doesn’t come for free - there is additional overhead in context switching, data being shuffled on and off the heap, etc which is why async code can be a bit slower than the equivalent sync code if there is plenty of available threads in thread pool. This difference is usually very minor though.

It’s a good idea to write async action methods that are I/O bound even if the API is only currently dealing with a low amount of usage. It only takes typing an extra keyword per I/O call and the usage can grow.

If you to learn about using React with ASP.NET Core you might find my book useful:

ASP.NET Core 5 and React

ASP.NET Core 5 and React
Find out more

Want more content like this?

Subscribe to receive notifications on new blog posts and courses

Required
© Carl Rippon
Privacy Policy