Austin Wise
Go to home page
2021-06-06

Async Part 1 - Why Async

In 2012 C♯ 5 introduced the await and async keywords for expressing asynchronous operations. Since then the model has become pervasive, spreading to languages like JavaScript, Python, and C++. Over a series of articles I plan to explain why async is useful and how some languages implement it.

Why care about async anyways

Before we get into how async works, let's take a step back and talk about why it is useful. Imagine you are building a website or a mobile application. People using your application are going to make requests to view information. Your application server probably does not have all the information needed to answer these requests. It is going to have to query databases, caches (memcache, redis, etc.), or microservices:

User requesting content from a webserver and the webserver querying backends.

When getting all the data needed to render a webpage, the webserver will need to make a lot of queries. Each query takes some time to travel across the network and complete. If we wait for one request to complete before issuing the next, it can take a long time:

Requests to backends being executed sequentially.

If we can issue many requests at the same time, the total amount of time to get all the data should be shorter:

Requests to backends being executed concurrently.

The key property in an async system is "non-blocking". The thread of execution is not blocked while a task is performed asynchronously.

Systems that predate async/await

The idea of concurrently executing many different IO operations at once is pretty old. Before async/await, asynchronous programming was more cumbersome due to lack of language support. Old programming models include:

  • Creating a new thread for every operation
  • Green threading schedulers (libtask, Go-lang)
  • Callbacks
  • Promises
  • Polling
  • Eventloops
  • Asynchronous Procedure Calls!

Each of these approaches has advantages and disadvantages. Using blocking calls on manually managed threads can be expensive, as every thread requires resources like stack virtual memory and operating system bookkeeping. I'll go into this more in my article of about schedulers.

Green thread scheduler systems improve on the resource utilization of thread threads by managing the task switching in user mode instead of the operating system. They provide APIs that look pretty similar to blocking APIs. The downside is the concurrent behavior is hidden behind these API calls and the APIs provided to manage concurrent tasks are fairly low level: things like locks and communication channels. So the programmer still has to do a lot of work when dealing with multiple concurrent tasks.

The rest of the APIs styles all require the programmer to radically re-architect their program to fit into the system. A well known contemporary example is callback hell in JavaScript. Before await was added to JavaScript, asynchronous programming typically required writing callback functions for each asynchronous operation. It is hard enough to express and understand the control flow when everything goes right. Handling errors is even more complicated:

function downloadContactList(onContactsDownloaded, onContactsFailure) {
    serviceLocator.findServer('contacts', function (serverName, locateFailure) {
        if (locateFailure) {
            onContactsFailure(locateFailure);
        }
        else {
            webClient.downloadJson(serverName, '/contacts', function (jsonPayload,
                                                                      wcFailure) {
                if (wcFailure) {
                    onContactsFailure(wcFailure);
                } else {
                    var contacts = contactParser.parse(jsonPayload);
                    onContactsDownload(contacts);
                }
            });
        }
    });
}

The fundamental problem is these async systems make it difficult to compose multiple asynchronous operations with each other.

Enter async functions

With async/await, expressing an asynchronous operation looks much more like normal code:

async function downloadContactList(onContactsDownloaded, onContactsFailure) {
    try {
        let serverName = await serviceLocator.findServer('contacts');
        let jsonPayload = await webClient.downloadJson(serverName, '/contacts');
        let contacts = contactParser.parse(jsonPayload);
        onContactsDownload(contacts);
    } catch (error) {
        onContactsFailure(error);
    }
}

Next time I will write about the properties of the async/await language feature.