Python Asynchronous Programming: A Brief Overview

When people talk about program execution, then “asynchronous execution” means a situation where the program does not wait for the completion of a certain process, but continues to work independently of it. As an example of asynchronous programming, you can use a utility that, working asynchronously, writes to a log file. Although such a utility may fail (for example, due to a lack of free disk space), in most cases it will work correctly and can be used in various programs. They will be able to call her, passing her the data for recording, and after that they can continue to do their own thing.







The use of asynchronous mechanisms when writing a certain program means that this program will run faster than without using such mechanisms. At the same time, what is planned to be run asynchronously, such as a utility for logging, should be written taking into account emergencies. For example, a utility for logging, if the disk space runs out, can simply stop logging, and not "crash" the main program with an error.



Asynchronous code execution usually involves the operation of such code in a separate thread. This is - if we are talking about a system with a single-core processor. On systems with multi-core processors, such code may well be executed by a process using a separate core. A single-core processor at a certain point in time can read and execute only one instruction. It is like reading books. You cannot read two books at the same time.



If you are reading a book and someone else is giving you another book, you can take this second book and start reading it. But the first will have to be postponed. Multi-threaded code execution is arranged on the same principle. And if several of your copies would read several books at once, then it would be similar to how multiprocessor systems work.



If on a single-core processor it is very fast to switch between tasks that require different computing power (for example, between certain calculations and reading data from a disk), then it may feel like a single processor core does several things at the same time. Or, say, this happens if you try to open several sites in a browser at once. If the browser uses a separate stream to load each of the pages, then everything will be done much faster than if these pages would load one at a time. Page loading is not such a difficult task, it does not use the system’s resources to the maximum, as a result, the simultaneous launch of several such tasks is a very effective move.



Python Asynchronous Programming



Initially, Python used generator-based coroutines to solve asynchronous programming tasks. Then, in Python 3.4, the asyncio



module asyncio



(sometimes its name is written as async IO



), which implements asynchronous programming mechanisms. Python 3.5 introduced the async / await construct.



In order to do asynchronous development in Python, you need to deal with a couple of concepts. These are coroutine and task.



Coroutines



Typically, coroutine is an async function. Coroutine can also be an object returned from a coroutine function.



If, when declaring a function, it is indicated that it is asynchronous, then you can call it using the await



keyword:



 await say_after(1, 'hello')
      
      





Such a construction means that the program will be executed until it encounters an await-expression, after which it will call the function and pause its execution until the work of the called function is completed. After that, other coroutines will also be able to start.



Pausing a program means that control returns to the event loop. When using the asyncio



module, the event loop performs all asynchronous tasks, performs I / O, and performs subprocesses. In most cases, tasks are used to run corutin.



Tasks



Tasks allow you to run coroutines in an event loop. This simplifies the execution control of several coroutines. Here is an example that uses coroutines and tasks. Note that entities declared using the async def



construct are coroutines. This example is taken from the official Python documentation .



 import asyncio import time async def say_after(delay, what):    await asyncio.sleep(delay)    print(what) async def main():    task1 = asyncio.create_task(        say_after(1, 'hello'))    task2 = asyncio.create_task(        say_after(2, 'world'))    print(f"started at {time.strftime('%X')}")    #     (      #  2 .)    await task1    await task2    print(f"finished at {time.strftime('%X')}") asyncio.run(main())
      
      





The say_after()



function has the async



prefix; as a result, we have coroutine. If we digress a little from this example, we can say that this function can be called like this:



     await say_after(1, 'hello')    await say_after(2, 'world')
      
      





With this approach, however, coroutines are invoked sequentially and take about 3 seconds to complete. In our example, they are competitively launched. For each of them a task is used. As a result, the execution time of the entire program is about 2 seconds. Please note that for such a program to work, it is not enough to simply declare the main()



function with the async



. In such situations, you need to use the asyncio



module.



If you run the sample code, a text similar to the following will be displayed on the screen:



 started at 20:19:39 hello world finished at 20:19:41
      
      





Note that the timestamps in the first and last lines differ by 2 seconds. If you run this example with a sequential call of corutin, then the difference between the time stamps will be already 3 seconds.



Example



In this example, the number of operations required to calculate the sum of ten elements of a sequence of numbers is determined. Calculations are performed starting from the end of the sequence. A recursive function starts by getting the number 10, then calls itself with the numbers 9 and 8, adding up what will be returned. This continues until the calculations are completed. As a result, it turns out, for example, that the sum of a sequence of numbers from 1 to 10 is 55. At the same time, our function is very inefficient, the time.sleep(0.1)



construction is used here.



Here is the function code:



 import time def fib(n):    global count    count=count+1    time.sleep(0.1)    if n > 1:        return fib(n-1) + fib(n-2)    return n start=time.time() global count count = 0 result = fib(10) print(result,count) print(time.time()-start)
      
      





What happens if you rewrite this code using asynchronous mechanisms and apply the asyncio.gather



construct asyncio.gather



, which is responsible for performing two tasks and waiting for them to complete?



 import asyncio,time async def fib(n):    global count    count=count+1    time.sleep(0.1)    event_loop = asyncio.get_event_loop()    if n > 1:        task1 = asyncio.create_task(fib(n-1))        task2 = asyncio.create_task(fib(n-2))        await asyncio.gather(task1,task2)        return task1.result()+task2.result()    return n
      
      





In fact, this example works even a little slower than the previous one, since everything is executed in one thread, and calls to create_task



, gather



and others like that create additional load on the system. However, the goal of this example is to demonstrate the ability to compete in multiple tasks and to wait for them to complete.



Summary



There are situations in which the use of tasks and corutin is very useful. For example, if a program contains a mixture of input-output and calculation operations, or if different calculations are performed in the same program, you can solve these problems by running the code in a competitive rather than in sequential mode. This helps reduce the time required for the program to perform certain actions. However, this does not allow, for example, to perform calculations simultaneously. Multiprocessing is used to organize such calculations. This is a separate big topic.



Dear readers! How do you write asynchronous Python code?








All Articles