How does Python Asynchronous Programming work

October 06, 2022 • 485 Views • 16 min read

author photo

Tetiana Stoyko

CTO

It won’t be a big secret if we tell you that the number of various development solutions and approaches in the modern IT industry is enormous. Therefore, let’s be more specific and consider one such case - Python asynchronous programming, what it is, how it works, and what instruments you need to implement Python asynchronous function.

What Does Asynchronous Mean?

Clearly, asynchronous code is the opposite of synchronous code architecture. Yet, what is the difference between them? Synchronous architecture is a basic program development approach when the working process is working step-by-step. In other words, each next process starts after the previous one ended. For instance, each process performs data input and receives output from the server. Therefore, if there are operation 1 and process 2, as well as input/output 1 and input/output 2, input 2 starts after output 1. So, we can make a formula for synchronous architecture:

python-asyn.png

Unlike the synchronous approach, the asynchronous code does not require such a clear hierarchy. So, both hypothetical operations can be performed simultaneously and independently of each other. It is also known as concurrency running. So, the schematic formula of Asynchronous architecture can look like this:

python-asyn-1.png

As a result, the asynchronous architecture is a great solution for improving application performance. Thanks to the independence of various features and services the response rate increases, which results in higher performance. However, you may notice, that in this case there is no actual connection between the processes at all. As a result, each I/O request will be considered independent and in case, when the number of such requests is critical, the CPU core won’t be able to deal with them, resulting in a system crash. Also, there are various working approaches, worth additional explanation: Multiprocessing, Multitasking, and Concurrency.

Multiprocessing is probably the most common processing type when each task is performed independently and at the same time. In other words, multiprocessing supports the performing of a few tasks at the same time. Yet, in this case, it requires an additional CPU as well, because this approach considers each task as a standalone process, therefore they are continuous and are not interrupted.

Multitasking, on the contrary, is able to run more than one process at the same time on the same processor. It is possible due to the interchangeable nature of the processing. To make it simple, multiprocessing supports switching between the processes, which is known as context switching. There are two ways how to perform effective multitasking: preemptive, when short time periods of CPU use are parceled to each process, and cooperative, when each process is able to use CPU for as long as needed.

To rephrase it, the preemptive approach considers each task as a dependent object. At the same time, the cooperative one sees these tasks as a subject, allowing them to define and fulfill their requirements on their own, still correlating with the concept of partnership and the importance of sharing resources. Eventually, Concurrency is an approach, that supports the processing of multiple tasks at the same time. The uniqueness of this operational type is that the processes are executed in sequence, yet this sequence is flexible. To put it differently, despite the sequence, the processes may communicate or interrupt each other. These sequences are known as threads. Therefore, a single-threaded program is one, that executes only one sequence/thread. Eventually, the multithreading program is executing more than one thread at the same time.

It is possible to say, that within a single thread, operations are processed in a strict order, while in the case of multithreading, threads are regularly interrupting each other and being executed. This allows us to schematize and prioritize them to make the program work faster. For instance, if there is a resource-intensive task, it is lowly prioritized and frozen, until it is the only process left.

This allows to fastly execute less intensive tasks and avoid additional waiting time.

In the case of Asyncio applications, they provide both single-thread and single-process options. Yet, the foregoing facts are not final. For instance, apart from (a)synchronous processes, various approaches to performing such programming types exist. For instance, to better understand the difference between these processes you also need to know some essential terms, we will meet in the future.

Explaining Some Detail

In Python there are various terms, that may seem similar at the first glance. For instance, threads, tasks, processes, or operations. However, it is important to understand, that behind their similarity also lies the distinction.

So, threads are used to structure the order of performing various tasks or requests. Thanks to the threads it is possible to line up different processes. As a result, in the case of concurrent programming, if a previous action is more complex and requires some time to be performed, the next one can waits until the first one is completed, so it will start exactly at the moment, the processor can perform it. Alternatively, if the threaded program knows the time, required for the response, it can switch to the awaiting task, completing it, while the previous task is waiting for the response from the server.

Additionally, asyncio requires an event loop feature, which is used to measure the progress of tasks routinely. As a result, when the system sees the progress in a task, it can automatically start the next one, avoiding idle awaiting. Therefore, due to the event loop, it is possible to automate the queue, starting the next task as soon as possible.

There is a difference between concurrent and parallel programming systems as well. The concurrent code performs different operations one by one, allowing them to save progress and switch to more prioritized, or pause the less urgent process. On the other hand, the parallel structure performs various tasks simultaneously, without the possibility to reschedule other actions. As a result, concurrent code allows performing various stages of each task while they are waiting and served one by one, while a parallel approach can simultaneously operate a few tasks at a time, yet during this process, others are paused.

Eventually, it is possible to state, that the best use cases for asynchronous programming are working with data, databases, files, etc. In fact, most I/O-related processes can be easily matched with the asynchronous approach. It is also possible to try using it in other processes, yet it can regularly cause working issues because there is no sense.

What Does Asynchronous Programming Require?

In the case of Python, you will definitely need the Asyncio library. It is a standard library, used for Python asynchronous operations. Clearly, there are various alternatives, yet most of them are built on top of the Asyncio library. It enables using async/await syntax, which is crucial for developing asynchronous programs. Therefore, the asyncio library is a must-have tool for further work with Python asynchronous programming.

Additionally, you will definitely need to know the basics of working with event loops, how to implement them, what their working specifics are, what is concurrency, etc. As was mentioned before, event loops allow starting a neverending process, as well as tracking the progress of various tasks, managing the queue line, delegating priority, etc.

Also, you will need to define coroutines. Coroutines are one of the subroutine forms. In other words, you can use coroutines in multiple ways, which allows for building a more flexible structure. Combined with the async/await syntax, they are probably the best possible way of implementing asyncio-based apps.

As we already stated, the best use cases of asynchronous programming - is working with databases, files, or information. Therefore, it is possible to assume, that to work with this programming approach you will also need a database or its analogs. For some cases you may want to need a message broker for some asynchronous use cases. For instance, it can be a great tool for projects, based on microservices architecture.

Asynchronous Python Code Samples

As we mentioned before, the best scenario to use asynchronous programming in Python - is to work with the databases and with the use of coroutines. In the case, as an example, we used ArrangoDB - a free graph database, RabbitMQ - a free message broker, asyncio library, and coroutines. First of all, let’s connect our database and declare the coroutine:

from aioarango import ArangoClient
 
async def arango_context(db_name, host, username, password):
       client = ArangoClient(hosts=host)
       return await client.db(name=db_name, username=username, password=password)

In this case, the “arango_context” function is a coroutine. So, we need to use an “await” keyword when we call it:

class Integration:

   async def db(self):
       return await arango_context(
           db_name=AS.DATABASE, host=AS.HOST,
           username=AS.USER, password=AS.PASSWORD)

Let’s also create a sample dataclass, which will retry running a callback function. For example, we need to test if the data was transformed and saved successfully to the database. Saving data to the database can take some time, so we will try to get data after sleeping for 3 seconds. Only after the 5th failed attempt, an exception will arise:

class Retry:
 
   async def run(self, callback_func, *args):
       result = None
       retry = 0
       while True:
           if not result and retry >= 5:
               raise Exception('Retry exhausted')
 
           try:
               result = await callback_func(*args)
               if result:
                   return result
 
           except Exception as exc:
               key = f'for {args[0].key}' if args else ''
               logger.info('Got an error %s, will retry. Exception: %s', key, exc)
 
           retry += 1
           await asyncio.sleep(3)

Yet, as we mentioned before, we also need an event loop, because it is a crucial aspect of asynchronous programming. Therefore, we also need to set an event loop, if we want your future program to work. The next Python code sample is an example of how to consume multiple queues concurrently using Python and Asyncio. First of all, we need to create an event loop. When the service is stopped, the connection to the RabbitMQ will be closed. Loop will run forever waiting for a RabbitMQ message to the specific processor queue:

async def main(loop):

   client = Client(settings=rabbit_settings)
   await client.build_client()
   processors = [
       CompanyUpdatedProcessor,
       CompanyInvalidatedProcessor,
       CompanySyncProcessor,
   ]
   await client.subscribe(processors)
   return client.connection
 
 
if __name__ == '__main__':
   logger.info('Service sync started.')
   loop = asyncio.get_event_loop()
   connection = loop.run_until_complete(main(loop=loop))
   try:
       loop.run_forever()
   except KeyboardInterrupt:
       logger.info('Service sync stopped.')
   except Exception as exc:
       logger.exception(exc)
   finally:
       loop.run_until_complete(connection.close())

In fact, the Asyncio library has more interesting methods to use in different cases. For instance, here is one more simple example which waits until the created task completes:

   async def consume(self, context, channel, message):
       task = asyncio.create_task(self.send_message(context, channel, message))
       done, pending = await asyncio.wait({task}, timeout=None, return_when=ALL_COMPLETED)

The “create_task” method submits the coroutine to run "in the background", i.e. together with the current task and all other actions to perform, changing them at awaiting points. It returns an awaitable object, that can be used to cancel the execution of the coroutine. It's one of the main asyncio principles, an equivalent of starting a thread. When you call “create_task”, you submit a coroutine for implementation and receive back an object. You can await this object when you actually need the result, or you can never await it if you are not interested in the result.

End Line

Summing up all the above, we can agree that Python’s asynchronous function can be a very powerful instrument to improve the performance and response rate. Yet, it is worth mentioning, that using such a method requires some additional skills. For instance, apart from understanding the working principles of software and technologies like Asyncio and how to use async/await syntax, developers have to know how to create event loops. In fact, at the moment there is a variety of additional tools for asynchronous architecture development. As an example, FastAPI, a framework that is gaining popularity thanks to its high performance and intuitiveness.

Actually, asynchronous programming is not limited only to Python, it is possible to use the asynchronous approach in other programming languages like JavaScript, C#, and others. Among the best asynchronous usage purposes, it is possible to highlight working with databases, files, and I/O-related operations. Also, it can be used for high-level structured network code, or in microservices architecture cases.

Share this post

Tags

Tech
Guide
Case Studies

What’s your impression after reading this?

Love it!

Valuable

Exciting

Unsatisfied

Got no clue where to start? Why don’t we discuss your idea?

Let's talk!

Contact us

chat photo
privacy policy

© 2015-2024 Incora LLC

offices

Ukrainian office

116, Bohdana Khmel'nyts'koho, Lviv, Lviv Oblast, 79019

USA office

16192 Coastal Hwy, Lewes, DE 19958 USA

follow us

This site uses cookies to improve your user experience.Read our Privacy Policy

Accept