Beginning Python Programming — Part 13
By: Bob Roebling
Tags: Coroutine, Python, Async, Programming, Asyncio

Beginning Python Programming — Part 13

Diving into asynchronous code

Photo by amirali mirhashemian on Unsplash

In the previous article, we covered iterators and generators.

In this article, we are going to dive into asynchronous code, or code that can do multiple things at once. Just a word of warning, this lesson is going to be hard. It’s going to require that you have a good grasp on everything we’ve covered so far. The good news is that after this, the rest will be easy in comparison.

While most articles use sleep() when giving examples of how async programming works, I promised a friend that I would avoid using this syntax to explain async code. All you need to know is that when you call sleep(3) the program’s execution will wait for 3 seconds before resuming again.

Before we dive in, we need to cover a few terms that will be used throughout this article.

Coroutine — the lowest level of async programming, this is a function that does stuff asynchronously. These can be run directly or used in a task.

Task — used to schedule coroutines for async execution by the system.

Future — used to place coroutines on hold until another coroutine is completed. (think of this as background code that needs to fetch data before it can complete)

Event Loop — a loop that iterates over one or more tasks until completed. In a web server, this might be an infinite loop waiting for a client connection. This is the core of async in Python; it orchestrates all of the work done in the background.

CPU — Central Processing Unit; the piece hardware in your computer that performs calculations (i.e., work).

CPU Physical Core — CPUs today contain multiple cores. These cores may include logical processors (e.g., Intel i3, i5, i7) each physical core is responsible for processing data or handing off work to a logical processor. You can touch physical cores if you tear down a CPU.

Logical Processor — If a physical core contains logical processors, these perform work. Logical processors only exist because of software, but this allows your computer to do multiple things at the same time. (Streaming music while writing code while downloading a file, etc.) . Logical processors are the reason you see eight cores on a quad-core machine.

Thread — a queue within a physical or logical processor that performs work. Processors can contain multiple queues; they are just scheduled based on priority. Since computers have multiple cores, your code may run on the main thread of one core while your user interface runs on the main thread of another. Any code that does not run on the main thread is considered to run in the background.

Awaitables

asyncio is one module we can import to perform asynchronous code. It is designed for situations where you have many connections with slow I/O (input/output) and provides two utilities that matter right now — async and await . The good example where asyncio would be used would be a web server.

An awaitable is an object that can be used in an await expression. async is used in front of def for any function that contains await .

Coroutines and Futures

While I gave you a definition above, I wanted you to have a basic understanding of the big picture before getting into detail.

I know this looks like a lot of code, but we will work through it one section at a time. This is a rework of a script I found here.

At the top, we have two imports: asyncio and json . asyncio is what will be doing the heavy lifting when returning data to clients and json is just used to encode our dictionary to a JSON structure that we can return to each client.

The next method async def connection(reader, writer) is used to respond to a client when it asks for data. Because we used async , this is a coroutine object that will run on a separate thread so the server can process other requests while this is returning a response. reader is used to pass the request to our function, and writer forms a response to return to the client.

First, we read the request using await reader.read(1024) . When we ask for something, there is usually some data that comes with the request. This data tells us what the client wants to do and how it wishes to do it. It’s up to our server to fulfill this response as long as it is valid. Here we are reading the first 1024 bytes which should be enough for any GET request. We use await to put this method on hold until all of the data can be read. This means that reader.read(1024) is a future.

Next, we begin building our response. Since we need to tell the client how to understand the data we are sending back, we need to include a header.

In this header, we tell the client a few things:

HTTP/1.1 — the version of HTTP we are responding with

200 OK — the status code of the request, 200 means successful

Content-Type: application/json — the content type we are returning will be JSON.

You might notice we have \r

included in the request, and twice at the end.

\r is an escape sequence that creates a carriage return (CR). This is representative of old typewriters when you hit the edge of the page. A carriage return was needed to bring the carriage (the bar that holds the paper) back to the left margin of the page. \r was used for new lines for Mac OS versions earlier than X.



is an escape sequence that creates a line feed (LF). Back to the typewriters; a line feed was when you advanced the page down to start a new line.

is still in use in Linux and macOS systems today to represent new lines.

\r

is the idea of doing both a carriage return and a line feed (CRLF). While it is a more accurate description of what happened when people were using typewriters, it was also adopted for creating new lines in Windows. It is also a big reason why your code sometimes doesn’t work when you switch between Windows and Linux (or Windows and macOS).

Ok, back to it. Next up, we have data which is just a random bit of JSON that I created using this script, modifying slightly for brevity, and stored it as a dictionary in the snippet.

Next, we convert our dictionary to a JSON string using json.dumps(data) . dumps stands for “dump string.” Since our web server only likes dealing in bytes, we cast it to a bytes object, giving it a UTF-8 encoding so our client can parse it correctly.

We then use writer.write(header + body) . First, it concatenates header with body , then we write this data back to the web server to return to the client before closing our writer, which effectively flushes the writer’s buffer and deletes the object from memory.

async def main(host, port) is a coroutine used for handling the requests that come into the web server. It accepts a host and a port as parameters which will be used in the body to create a basic web server.

server = await asyncio.start_server(connection, host, port) is long but very simple to understand. We create a server object using asyncio.start_server . This is a future that will create a web server using our connection() coroutine from above, a host , the IP address of the server this will be hosted on, and the port on the server that we will accept connections from.

async with server is new syntax that we haven’t covered yet. with essentially does the creation and any cleanup needed after we are finished with whatever we are trying to do. async with server kicks off the server using async , and if we run into any issues, it shuts down the server cleanly. Here we use it to run a future await server.serve_forever() . That’s right, the server that was created for us above also comes with an async function.

Finally, we need to set up the server.

asyncio.run() creates an event loop that allows us to run our coroutines. main("0.0.0.0", 8000) is the coroutine that we pass in because it will eventually call our connection coroutine.

If you are unfamiliar with IP addresses, here’s a quick rundown:

127.0.0.1 — there’s no place like home, this is your local loopback address.

0.0.0.0 — this tells the server to host on all IP addresses on all interfaces (network cards). If you have both an Ethernet port and a wireless card, the server will be available to both interfaces. Be careful with this; you may want to specify the IP address to only one network interface, Ethernet is always preferred.

Every computer has a number of ports available to it, 65536 to be exact, although we start from 0, so the maximum port number is 65535. Be sure to review the well-known ports (there are 1024 of them). You should stay away from these as much as possible during testing since they are reserved for specific functions. Port 80 is for HTTP and 443 is for HTTPS. While I can’t say I’ve remembered all of them, I remember the ones I use the most.

There are other ports you should watch out for, such as PostgreSQL — 5432, MSSQL — 1433, 1434, MySQL — 3306, and RDP — 3389.

These are essential things you will become familiar with the more you use them.

Back to it! You might notice that I wrapped this in a try/except block. I did this because when I pressed ctrl+c on my keyboard, the program crashed. I didn’t like it, so I handled the exception KeyboardInterrupt which occurs when the execution is broken by the user. I could have just passed and let it end quietly, but decided to print “Stopping web server” before exiting just to be friendly to the web admin.

Photo by Glenn Carstens-Peters on Unsplash

Tasks

When it comes to tasks, the first thing to remember is that tasks are not thread-safe. What this means is that if data is changed by one coroutine that another coroutine uses, there could be some unexpected results, or worse, your program could crash.

In terms of thread safety, think of it like this: We both drive the same car. We have to plan around who drives at what time. If I drive the car to work between 8–5 and you try to drive the car at 10 am, you won’t be able to because the car isn’t there.

Translated: Let’s assume we have a buffer that can contain any data we want to store in it. Then we have two tasks that use this same buffer to perform work. One task uses an integer data type, and the other uses strings.

The first task stores 42 inside of the buffer, the second task replaces the contents of the buffer with “Hello,” then the first task tries to use the buffer to add 1 to the buffer… Do you see how this could create a problem for us?

With that warning out of the way, let’s dig into tasks.

Tasks are used for scheduling coroutines. Tasks can also be used to have multiple coroutines run at the same time.

There are several methods available to tasks that allow you to cancel tasks, return results from tasks, inspect the status of a task, and add or remove callbacks to tasks.

A callback is essentially a method or function that a task will call when it is completed. If I had a task that printed “Bob” to the screen, it might have a callback that called a function that printed “Program finished” when it finished.

Time for an example that I borrowed from this page.

As usual, we import asyncio . We then have a count function which takes the current run number as an argument. Then we print 100 iterations for each run. We’ve done all this before.

create_tasks is an async function which creates an inner event loop using asyncio.get_event_loop() . It provides an event loop that we can use to execute tasks on.

I then created a list comprehension that generates 300 tasks to run. Inside of this list comprehension, we use inner_loop.run_in_executor . The first parameter of this function wants a concurrent.futures.executor instance. If we pass in None as we did here, we use the default executor. This is fine for our needs. The second parameter, count , refers to the function that we wish to call concurrently, that is, at the same time. Finally, i is the argument that we pass in, which will be handed off to the count function when it is called.

Finally, we use for _ in await asyncio.gather(*tasks) to add all of our tasks into a future that aggregates all of our tasks into one result.

Our program will start off creating an outer loop which will be used to schedule the inner loop, telling it to run_until_complete and passing in the function create_tasks() as the only task it will need to perform.

Once the task is complete, the outer loop closes. While it may appear that we forgot to close the inner loop, it was automatically closed when create_tasks ended. If we attempted to close it at the end of the function, we would get a runtime error that we cannot close a running event loop.

I want you to run this code and look for anything that appears strange in the output. If you use small ranges, you’ll see fewer anomalies, perhaps none. But don’t worry if you do, because we didn’t create all of these tasks in a thread-safe way. If we wanted to do that, we’d handle each task’s result one at a time if they were all being stored in the same place.