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?