Введение в asyncio на Python
Здравствуйте! Я хочу начать эту статью с вопроса, можете ли вы отсортировать список асинхронно используя время? Ответим на этот вопрос в конце статьи.
Что такое асинхронное программирование?
Давайте сначала определим две понятия, параллелизм и конкурентность
параллелизм — это выполнение нескольких задач одновременно для ускорения вашей программы,
конкурентность — это управление выполнением разных задач, но при этом задачи необязательно выполняются одновременно, поэтому их можно разделить на более мелкие и чередующиеся.
Я объясню эти термины на простом примере:
- представьте, что 3 студента собираются сдавать экзамен, учитель дает им листок, ждет пока студенты выполнят и затем начинает проверять — это конкурентность. И пока учитель ждет ответы на проверку, он может заниматься другими задачами, например читать книгу, проверять других студентов и прочее.
- а теперь представьте, что трое учителей будут принимать экзамен, то есть каждый учитель будет принимать у одного студента и выставлять оценки как только студент завершит задачу — это параллелизм.
Асинхронное программирование — это такой метод программирования, при котором поток выполняя задачу не ждет завершения этой задачи, пока задача выполняется он храня информацию о состояния задач пытается выполнять другие задачи и получать результаты. Например, можем привести пример утилиту, которая, работая асинхронно, делает записи в лог-файл. При этом он не будет блокировать основной поток и основная программа не будет ждать завершения записи в файл каждый раз.
Как реализовать модель асинхронного программирования на Python?
Для реализации асинхронных методов у Python есть пакет asyncio, в нем есть два ключевых слова async для определения сопрограмм (coroutines) и await для запуска сопрограмм.
Сопрограммы (coroutines)— это такие же обычные функции, за исключением того, что они останавливают свое выполнение до завершения функции и позволяют программе делать что-то еще.
Давайте посмотрим на примере:
Еще одна немаловажная функция asyncio.sleep(), которая работает так же, как time.sleep, за исключением того, что она работает асинхронно, что позволяет не останавливаться во время sleep.
Для запуска асинхронной программы используется eventloop, который заботится о запущенных сопрограммах. Но мы не будем его использовать, так как документация гласит использовать функцию высокого уровня asyncio.run() .
Итак для запуска асинхронных сопрограмм будем использовать asyncio.run(). Тут нужно запомнить 2 момента:
- эта функция была добавлена только начиная с версии 3.7
- функция не будет вызван если выполняется другой eventloop
Ибо в документации Python так и написано:
Эта функция всегда создает новый цикл событий и закрывает его в конце. Его следует использовать в качестве основной точки входа для программ asyncio, и в идеале его следует вызывать только один раз.
Посмотрим на примере:
Напомним что main() — это сопрограмма (coroutine), а не простая функция, поэтому добавляем его в asyncio.run
А что делать если сопрограмм в одном скрипте несколько? Используем asyncio.gather
Как мы видим, 3 задачи выполнились за 1 секунду вместо 3х!
Как использовать asyncio для сортировки
Теперь у вас достаточно знаний, чтобы ответить на вопрос который мы задали в начале статьи.
Рассмотрим сортировку целых чисел [1, 2, 2, 6, 3, 5] с помощью асинхронной функции:
Надеюсь статья была легкой для чтения и понимания основ асинхронного программирования!
Более подробно вы можете почитать в документации Python в разделе asyncio.
Python Asyncio Part 2 – Awaitables, Tasks, and Futures
Having already covered the basic concepts in Python Asyncio Part 1 – Basic Concepts and Patterns, in this part of the series I will be going into more depth on the actual syntax used when employing this library in Python code. Many of the examples used here are based on code we have actually used as part of BBC R&D’s cloudfit project.
Writing Asynchronous Code
The most basic tool in the tool kit of an asynchronous programmer in Python is the new keyword async def , which is used to declare an asynchronous coroutine function in the same way that def is used to define a normal synchronous function.
TERMINOLOGY: In this article I will refer to async def as a keyword, and in future articles I will refer to async for and async with as keywords. Strictly speaking this isn’t true. In fact async is a keyword and so is def , but since you can’t use async by itself, only in combination with another keyword I think it’s much more convenient and less confusing to think of async def as a single keyword that happens to have a space in the middle of it. It certainly behaves like one in terms of language usage.
In the above example we define a coroutine function example_coroutine_function and an ordinary function example_function . The code block that forms the body of the definition is slightly different in the two cases. The code block for example_function is ordinary synchronous Python, whilst the code-block for example_coroutine_function is asynchronous Python.
- Asynchronous Python code can only be included inside a suitable context that allows it, which almost always means inside a coroutine function defined using async def . There’s one other context where asynchronous code is allowed which we will cover in the next article.
- Asynchronous Python code can use any of the Python keywords, structures, etc… allowed in ordinary Python. Nothing is disallowed (although some things may be discouraged, see later).
- There are several new keywords which can only be used inside asynchronous code: await , async with and async for .
- Note that async def is not one of the keywords reserved for use in asynchronous code. It can be used anywhere were def can be used, though its effect is slightly different.
A declaration of a coroutine function using async def looks deceptively similar to the declaration of an ordinary function using def . Most of the time writing one is pretty similar, however there are some key differences, which are very important for asynchronous programming:
-
The Python def keyword creates a callable object with a name, when the object is called the code block of the function is run. Eg.
means that example_function is now a callable object which takes three parameters. When you invoke it like so:
this causes the function code to be run immediately as a subroutine call, and its return value to be assigned to r .
means that example_coroutine_function is now a callable object which takes three parameters. When you invoke it like so:
this does not cause the function code block to be run. Instead an object of class Coroutine is created, and is assigned to r . To make the code block actually run you need to make use of one of the facilities that asyncio provides for running a coroutine. Most commonly this is the await keyword. The function asyncio.gather is used in an example below. Other examples can be found in the python docs. See for example wait .
- The code block of asynchronous code inside an async def statement.
- The callable object that the async def statement creates.
- The object of class Coroutine that is returned by the callable object when it is called.
TYPING NOTE: If you are using the typing library then the declaration of coroutine functions can be a little confusing at times.
defines example_coroutine_function as a callable that takes two parameters of types A and B and returns an object of type Coroutine[Any, Any, C] . It’s pretty rare that you’ll need to refer to this return type explicitly.
If you’re curious about the two Any type parameters in the above definition they’re related to the way that the event loop works. The first type parameter actually indicates the type of the values that the coroutine will pass to the event loop whenever it yields, whilst the second represents the type of the values that the event loop will pass back to the coroutine whenever the it is reawakened. In practice the actual types of these objects are determined by the internal machinery of the event loop’s implementation, and should never need to be referred to explicitly in client code unless you are writing your own event loop implementation (which is a pretty advanced topic way beyond the scope of these articles).
The await Keyword and Awaitables
One of the new keywords added to the language to support asyncio is await . This keyword is, in many ways, the very core of asynchronous code. It can only be used inside asynchronous code blocks (ie. in the code block of an async def statement defining a coroutine function), and it is used as an expression which takes a single parameter and returns a value.
is a valid Python statement which will perform the await action on the object a and return a value which will be assigned to r . Exactly what will happen when this await statement is executed will depend upon what the object a is.
A coroutine object is “awaitable” (it can be used in an await statement). Recall that when you are executing asynchronous code you are always doing so in the context of a “Task”, which is an object maintained by the Event Loop, and that each Task has its own call stack. The first time a Coroutine object is awaited the code block inside its definition is executed in the current Task, with its new code context added to the top of the call stack for this Task, just like a normal function call. When the code block reaches its end (or otherwise returns) then execution moves back to the await statement that called it. The return value of the await statement is the value returned by the code block. If a Coroutine object is awaited a second time this raises an exception. In this way you can think of awaiting a Coroutine object as being very much like calling a function, with the notable difference that the Coroutine object’s code block can contain asynchronous code, and so can pause the current task during running, which a function’s code block cannot.
In fact there are three types of objects that are awaitable:
- A Coroutine object. When awaited it will execute the code-block of the coroutine in the current Task. The await statement will return the value returned by the code block.
- Any object of class asyncio.Future which when awaited causes the current Task to be paused until a specific condition occurs (see next section).
- An object which implements the magic method __await__ , in which case what happens when it is awaited is defined by that method.
That last one is there so that writers of libraries can create their own new classes of objects which are awaitable and do something special when awaited. It’s usually a good idea to make your custom awaitable objects either behave like a Coroutine object or like a Future object, and document which in the class’s doc strings. Making custom awaitable classes like this is a somewhat more advanced topic, though one that may come up when writing asyncio wrappers for synchronous io libraries, for example.
TYPING NOTE: If you are using typing then there is an abstract class Awaitable which is generic, so that Awaitable[R] for some type R means “anything which is awaitable, and when used in an await statement will return something of type R ”.
One of the most important points to get across is that the currently executing Task cannot be paused by any means other than awaiting a future (or a custom awaitable object that behaves like one). And that is something which can only happen inside asynchronous code. So any await statement might cause your current task to pause, but is not guaranteed to. Conversely any statement which is not an await statement (or an async for or async with under certain circumstances which will be explained in the next post) cannot cause your current Task to be paused.
This means that the traditional multithreaded code problems of data races where different threads of execution both alter the same value are severely reduced in asynchronous code, but not entirely eliminated. In particular for the purposes of data shared between Tasks on the same event loop all synchronous code can be considered “atomic”. To illustrate what this means consider the following code:
then even though both fetcher and monitor access the global variable vals they do so in two tasks that are running in the same event loop. For this reason it is not possible for the print statement in monitor to run unless fetcher is currently asleep waiting for io. This means that it is not possible for the length of vals to be printed whilst the for loop is only part-way through running. So if the get_some_values_from_io always returns 10 values at a time (for example) then the printed length of vals will always be a multiple of ten. It is simply not possible for the print statement to execute at a time when vals has a non-multiple of ten length.
On the other hand if there was an await statement inside the for loop this would no longer be guaranteed.
NOTE: Note that the create_task calls above are redundant. The body of main could be reduced to await asyncio.gather(fetcher(), monitor()) .
Futures
A Future object is a type of awaitable. Unlike a coroutine object when a future is awaited it does not cause a block of code to be executed. Instead a future object can be thought of as representing some process that is ongoing elsewhere and which may or may not yet be finished.
When you await a future the following happens:
- If the process the future represents has finished and returned a value then the await statement immediately returns that value.
- If the process the future represents has finished and raised an exception then the await statement immediately raises that exception.
- If the process the future represents has not yet finished then the current Task is paused until the process has finished. Once it is finished it behaves as described in the first two bullet points here.
All Future objects f have the following synchronous interface in addition to being awaitable:
- f.done() returns True if the process the future represents has finished.
- f.exception() raises an asyncio.InvalidStateError exception if the process has not yet finished. If the process has finished it returns the exception it raised, or None if it terminated without raising.
- f.result() raises an asyncio.InvalidStateError exception if the process has not yet finished. If the process has finished it raises the exception it raised, or returns the value it returned if it finished without raising.
It’s important to note that there is no way for a future that is done to ever change back into one that is not yet done. A future becoming done is a one-time occurrence.
IMPORTANT!: The distinction between a Coroutine and a Future is important. A Coroutine’s code will not be executed until it is awaited. A future represents something that is executing anyway, and simply allows your code to wait for it to finish, check if it has finished, and fetch the result if it has.
IMPORTANT!: Objects which implement the __await__ magic method may do almost anything when awaited. They might behave more like Coroutines, or more like Futures. They may do something else entirely. The documentation for the class in question should usually make it clear what their behaviour is.
You probably won’t create your own futures very often unless you are implementing new libraries that extend asyncio. However you will find that library functions often return futures. If you do need to create your own future directly you can do it with a call to
On the other hand you will probably find that you use a related method, create_task quite often …
TYPING NOTE: If you want to specify that a variable is a Future then you can use the asyncio.Future class as a type annotation. If you want to specify that the Future’s result should be of a specific type, R then you can use the following notation:
(in Python 3.6 you will need to wrap asyncio.Future[R] in quotes for this to work correctly, but in later versions of Python this is no longer needed).
Tasks
As described in the previous article each event loop contains a number of tasks, and every coroutine that is executing is doing so inside a task. So the question of how to create a task seems like an important one.
Creating a task is a simple matter, and can be done entirely in synchronous code:
NOTE: In Python 3.6 the function asyncio.create_task is not available, but you can still create a task using:
this is exactly the same, but a little more verbose.
The method create_task takes a coroutine object as a parameter and returns a Task object, which inherits from asyncio.Future . The call creates the task inside the event loop for the current thread, and starts the task executing at the beginning of the coroutine’s code-block. The returned future will be marked as done() only when the task has finished execution. As you might expect the return value of the coroutine’s code block is the result() which will be stored in the future object when it is finished (and if it raises then the exception will be caught and stored in the future).
Creating a task to wrap a coroutine is a synchronous call, so it can be done anywhere, including inside synchronous or asynchronous code. If you do it in asynchronous code then the event loop is already running (since it is currently executing your asynchronous code), and when it next gets the opportunity (ie. next time your current task pauses) it might make the new task active.
When you do it in synchronous code, however, chances are that the event loop is not yet running. Manualy manipulating event loops is discouranged by the python documentation. Unless you are developing libraries extending asyncio functionality, you should probably avoid trying to create a task from synchronous code.
If you do need to call a single piece of async code in an otherwise synchronous script, you can use asyncio.run() .
Running async programs
With the introduction of asyncio.run() in Python 3.7, and the removal of the loop parameter from many asyncio function in Python 3.10, managing event loops is something that you are unlikely to come across, unless you are developing an async library. The event loop objects are still there and accessible. There is a whole page in the docs discussing them. If you are working in Python 3.7 or greater, rejoice and give thanks for asyncio.run() .
asyncio.run(coro) will run coro , and return the result. It will always start a new event loop, and it cannot be called when the event loop is already running. This leads to a couple of obvious ways to run your async code.
The first is to have everything in async coroutines, and have a very simple entry function:
The second is to wrap each coroutine call in a separate run command. Note that this forgoes all of the benefits of asyncio. Still, there might be the odd script where this is the right thing to do.
Note that these simple examples don’t make use of the ability of async code to work on multiple tasks concurrently. A more sensible example is given at the end. As you work with asyncio in python, you’ll learn about more sophisticated ways to manage your work, but this is enough to get you started.
Manual event loop interaction
If you’re using Python 3.6, and you need to run coroutines from ordinary sync code (which you probably will, if you want to start something.) then you will need to start the event loop. There are two methods for doing this:
will cause the event loop to run forever (or until explicitly killed). This isn’t usually particularly useful. Much more useful is:
which takes a single parameter. If the parameter is a future (such as a task) then the loop will be run until the future is done, returning its result or raising its exception. So putting it together:
will create a new task which executes example_coroutine_function inside the event loop until it finishes, and then return the result.
In fact this can be simplified further since if you pass a coroutine object as the parameter to run_until_complete then it automatically calls create_task for you.
How to yield control
There is no simple command for yielding control to the event loop so that other tasks can run. In most cases in an asyncio program this is not something you will want to do explicitly, preferring to allow control to be yielded automatically when you await a future returned by some underlying library that handles some type of IO.
However occasionally you do need to, and in particular it’s quite useful during testing and debugging. As a result there is a recognised idiom for doing this if you need to. The statement:
will pause the current task and allow other tasks to be executed. The way this works is by using the function asyncio.sleep which is provided by the asyncio library. This function takes a single parameter which is a number of seconds, and returns a future which is not marked done yet but which will be when the specified number of seconds have passed.
Specifying a count of zero seconds works to interrupt the current task if other tasks are pending, but otherwise doesn’t do anything since the sleep time is zero.
The implementation of asyncio.sleep in the standard library has been optimised to make this an efficient operation.
When using asyncio.sleep with a non-zero parameter it’s worth noting that just because the future will become done when the number of seconds has passed does not mean that your task will always wake back up at that time. In fact it may wake back up at any point after that time, since it can only awaken when there’s no other task being run on the event loop.
Summary
- You can only use the keywords await , async with and async for inside asynchronous code.
- Asynchronous code must be contained inside an async def declaration (or one other place we’ll cover in the next article), but the declaration can go anywhere def is allowed.
- When you call await you must call it on one of the following:
- A coroutine object, which is the return value of a coroutine function defined using async def .
- The coroutine’s code will only be executed when it is awaited or wrapped in a task.
- Awaiting a future will not cause code to be executed, but might pause your current task until another process has completed.
- What happens then could be anything, check the documentation for the object in question.
Making an actual program
So that concludes our run down of the basic syntax for writing asynchronous code. With just this you can already create a perfectly good async program which can instantiate multiple tasks and allow them to be swapped in and out. The following example is a fully working Python program using only the things included in this post:
This program will run four tasks which print the numbers from 0 to 99, and after printing each task will yield control to allow other tasks to take over. It neatly demonstrates that asyncio allows multiple things to be done interleaved.
To actually do anything useful you’ll need to make use of one of the libraries that implement io, such as aiohttp, and when you do you might well find that there are a few things in their interfaces which I haven’t covered in this post. Specifically you’ll probably find that the interface makes use of async with and possibly also async for . So those will be the subject of the next post in this series: Python Asyncio Part 3 – Asynchronous Context Managers and Asynchronous Iterators
Rukovodstvo
статьи и идеи для разработчиков программного обеспечения и веб-разработчиков.
Учебное пособие по Python async / await
Асинхронное программирование набирает обороты в последние несколько лет, и не зря. Хотя это может быть сложнее, чем традиционный линейный стиль, он также намного эффективнее. Например, вместо ожидания завершения HTTP-запроса перед продолжением выполнения с помощью асинхронных сопрограмм Python вы можете отправить запрос и выполнить другую работу, ожидающую в очереди, пока ожидает завершения HTTP-запроса. Может потребоваться немного больше размышлений, чтобы понять логику,
Время чтения: 7 мин.
Асинхронное программирование набирает обороты в последние несколько лет, и не зря. Хотя это может быть сложнее, чем традиционный линейный стиль, он также намного эффективнее.
Например, вместо ожидания завершения HTTP-запроса перед продолжением выполнения с помощью асинхронных сопрограмм Python вы можете отправить запрос и выполнить другую работу, ожидающую в очереди, пока ожидает завершения HTTP-запроса. Чтобы понять правильную логику, может потребоваться немного больше размышлений, но вы сможете справиться с гораздо большей работой с меньшими ресурсами.
Даже тогда синтаксис и выполнение асинхронных функций в таких языках, как Python, на самом деле не так уж и сложны. Другое дело, с JavaScript, но Python, похоже, выполняет его довольно хорошо.
Асинхронность, кажется, главная причина того, почему Node.js так популярен для программирования на стороне сервера. Большая часть кода, который мы пишем, особенно в тяжелых приложениях ввода-вывода, таких как веб-сайты, зависит от внешних ресурсов. Это может быть что угодно, от удаленного вызова базы данных до POSTing в службу REST. Как только вы запрашиваете какой-либо из этих ресурсов, ваш код ждет, ничего не делая.
При асинхронном программировании вы позволяете своему коду обрабатывать другие задачи, ожидая ответа от других ресурсов.
Сопрограммы
Асинхронная функция в Python обычно называется сопрограммой, которая представляет собой просто функцию, использующую ключевое слово async @asyncio.coroutine . Любая из приведенных ниже функций будет работать как сопрограмма и фактически эквивалентна по типу:
Это специальные функции, которые при вызове возвращают объекты сопрограмм. Если вы знакомы с обещаниями JavaScript, то можете думать об этом возвращаемом объекте почти как об обещании. Вызов любого из них на самом деле не запускает их, но вместо этого возвращается объект сопрограммы , который затем может быть передан в цикл событий для последующего выполнения.
На случай, если вам когда-нибудь понадобится определить, является ли функция сопрограммой или нет, asyncio предоставляет метод asyncio.iscoroutinefunction(func) который делает именно это за вас. Или, если вам нужно определить, является ли объект, возвращаемый функцией , объектом сопрограммы, вы можете вместо этого asyncio.iscoroutine(obj)
Доходность от
Есть несколько способов вызвать сопрограмму, один из которых — метод yield from . Это было введено в Python 3.3 и было улучшено в Python 3.5 в форме async/await (о которой мы поговорим позже).
yield from может использоваться следующим образом:
Как видите, yield from используется в функции, украшенной @asyncio.coroutine . Если бы вы попытались использовать yield from извне этой функции, то получили бы такую ошибку от Python:
Чтобы использовать этот синтаксис, он должен находиться в другой функции (обычно с декоратором сопрограммы).
Асинхронный / ожидание
Более новый и чистый синтаксис заключается в использовании ключевых слов async/await Представленный в Python 3.5, async используется для объявления функции как сопрограммы, подобно тому, как это делает декоратор @asyncio.coroutine Его можно применить к функции, поместив его перед определением:
Чтобы вызвать эту функцию, мы используем await вместо yield from , но во многом таким же образом:
Опять же, как и yield from , вы не можете использовать это вне другой сопрограммы, иначе вы получите синтаксическую ошибку.
В Python 3.5 поддерживаются оба способа вызова сопрограмм, но async/await должен быть основным синтаксисом.
Запуск цикла событий
Ни один из описанных выше сопрограмм не будет иметь значения (или работать), если вы не знаете, как запускать и запускать цикл событий . Цикл событий — это центральная точка выполнения асинхронных функций, поэтому, когда вы действительно хотите выполнить сопрограмму, это то, что вы будете использовать.
Цикл событий предоставляет вам несколько функций:
- Регистрация, выполнение и отмена отложенных вызовов (асинхронные функции)
- Создавайте клиентские и серверные транспорты для связи
- Создавать подпроцессы и транспорты для связи с другой программой
- Делегировать вызовы функций пулу потоков
Хотя на самом деле существует довольно много конфигураций и типов циклов событий, которые вы можете использовать, большинству программ, которые вы пишете, просто нужно будет использовать что-то вроде этого для планирования функции:
Последние три строки — это то, что нас здесь интересует. Он начинается с получения цикла событий по умолчанию ( asyncio.get_event_loop() ), планирования и выполнения задачи async, а затем закрытия цикла, когда цикл завершается.
Функция loop.run_until_complete() фактически блокирует, поэтому она не вернется, пока не будут выполнены все асинхронные методы. Поскольку мы выполняем это только в одном потоке, он не может двигаться вперед, пока цикл выполняется.
Теперь вы можете подумать, что это не очень полезно, поскольку мы все равно блокируем цикл событий (а не только вызовы ввода-вывода), но представьте, что вы обертываете всю свою программу в асинхронной функции, которая затем позволит вам запускать много асинхронных запросы одновременно, как на веб-сервере.
Вы даже можете прервать цикл обработки событий в отдельный поток, позволяя ему обрабатывать все длинные запросы ввода-вывода, в то время как основной поток обрабатывает логику программы или пользовательский интерфейс.
Пример
Хорошо, давайте посмотрим на более крупный пример, который мы действительно можем запустить. Следующий код представляет собой довольно простую асинхронную программу, которая извлекает JSON из Reddit, анализирует JSON и распечатывает самые популярные сообщения дня из / r / python, / r / programming и / r / compsci.
Первый показанный метод, get_json() , вызывается get_reddit_top() и просто создает HTTP-запрос GET на соответствующий URL-адрес Reddit. Когда это вызывается с помощью await , цикл событий может продолжаться и обслуживать другие сопрограммы, ожидая ответа HTTP. Как только это произойдет, JSON возвращается в get_reddit_top() , анализируется и распечатывается.
Это немного отличается от примера кода, который мы показали ранее. Чтобы запустить несколько сопрограмм в цикле событий, мы используем asyncio.ensure_future() а затем запускаем цикл навсегда, чтобы обработать все.
Чтобы запустить это, вам нужно aiohttp , что вы можете сделать с помощью PIP:
Теперь просто убедитесь, что вы запускаете его с Python 3.5 или выше, и вы должны получить следующий результат:
Обратите внимание, что если вы запустите это несколько раз, порядок, в котором печатаются данные субреддита, изменится. Это связано с тем, что каждый из наших вызовов освобождает (передает) контроль над потоком, позволяя выполнить другой HTTP-вызов. То, что вернется первым, распечатывается первым.
Заключение
Хотя встроенные в Python асинхронные функции не так удобны, как JavaScript, это не значит, что вы не можете использовать их для интересных и эффективных приложений. Просто потратьте 30 минут, чтобы изучить его все входы и выходы, и вы лучше поймете, как вы можете интегрировать это в свои собственные приложения.
Что вы думаете об async / await в Python? Как вы использовали это в прошлом? Дайте нам знать об этом в комментариях!
Асинхронный python без головной боли (часть 1)
Асинхронное программирование традиционно относят к темам для «продвинутых». Действительно, у новичков часто возникают сложности с практическим освоением асинхронности. В случае python на то есть весьма веские причины:
Асинхронность в python была стандартизирована сравнительно недавно. Библиотека asyncio появилась впервые в версии 3.5 (то есть в 2015 году), хотя возможность костыльно писать асинхронные приложения и даже фреймворки, конечно, была и раньше. Соответственно у Лутца она не описана, а, как всем известно, «чего у Лутца нет, того и знать не надо».
Рекомендуемый синтаксис асинхронных команд неоднократно менялся уже и после первого появления asyncio . В сети бродит огромное количество статей и роликов, использующих архаичный код различной степени давности, только сбивающий новичков с толку.
Официальная документация asyncio (разумеется, исчерпывающая и прекрасно организованная) рассчитана скорее на создателей фреймворков, чем на разработчиков пользовательских приложений. Там столько всего — глаза разбегаются. А между тем: «Вам нужно знать всего около семи функций для использования asyncio» (c) Юрий Селиванов, автор PEP 492, в которой были добавлены инструкции async и await
На самом деле наша повседневная жизнь буквально наполнена асинхронностью.
Утром меня поднимает с кровати будильник в телефоне. Я когда-то давно поставил его на 8:30 и с тех пор он исправно выполняет свою работу. Чтобы понять когда вставать, мне не нужно таращиться на часы всю ночь напролет. Нет нужды и периодически на них посматривать (скажем, с интервалом в 5 минут). Да я вообще не думаю по ночам о времени, мой мозг занят более интересными задачами — просмотром снов, например. Асинхронная функция «подъем» находится в режиме ожидания. Как только произойдет событие «на часах 8:30», она сама даст о себе знать омерзительным Jingle Bells.
Иногда по выходным мы с собакой выезжаем на рыбалку. Добравшись до берега, я снаряжаю и забрасываю несколько донок с колокольчиками. И. Переключаюсь на другие задачи: разговариваю с собакой, любуюсь красотами природы, истребляю на себе комаров. Я не думаю о рыбе. Задачи «поймать рыбу удочкой N» находятся в режиме ожидания. Когда рыба будет готова к общению, одна из удочек сама даст о себе знать звонком колокольчика.
Будь я автором самого толстого в мире учебника по python, я бы рассказывал читателям про асинхронное программирование уже с первых страниц. Вот только написали «Hello, world!» и тут же приступили к созданию «Hello, asynchronous world!». А уже потом циклы, условия и все такое.
Но при написании этой статьи я все же облегчил себе задачу, предположив, что читатели уже знакомы с основами python и им не придется втолковывать что такое генераторы или менеджеры контекста. А если кто-то не знаком, тогда сейчас самое время ознакомиться.
Пара слов о терминологии
В настоящем руководстве я стараюсь придерживаться не академических, а сленговых терминов, принятых в русскоязычных командах, в которых мне довелось работать. То есть «корутина», а не «сопрограмма», «футура», а не «фьючерс» и т. д. При всем при том, я еще не столь низко пал, чтобы, скажем, задачу именовать «таской». Если в вашем проекте приняты другие названия, прошу отнестись с пониманием и не устраивать терминологический холивар.
Внимание! Все примеры отлажены в консольном python 3.10. Вероятно в ближайших последующих версиях также работать будут. Однако обратной совместимости со старыми версиями не гарантирую. Если у вас что-то пошло не так, попробуйте, установить 3.10 и/или не пользоваться Jupyter.
2. Первое асинхронное приложение
Предположим, у нас есть две функции в каждой из которых есть некая «быстрая» операция (например, арифметическое вычисление) и «медленная» операция ввода/вывода. Детали реализации медленной операции нам сейчас не важны. Будем моделировать ее функцией time.sleep() . Наша задача — выполнить обе задачи как можно быстрее.
Традиционное решение «в лоб»:
Пример 2.1
Никаких сюрпризов — fun2 честно ждет пока полностью отработает fun1 (и быстрая ее часть, и медленная) и только потом начинает выполнять свою работу. Весь процесс занимает 3 + 3 = 6 секунд. Строго говоря, чуть больше чем 6 за счет «быстрых» арифметических операций, но в выбранном масштабе разницу уловить невозможно.
Теперь попробуем сделать то же самое, но в асинхронном режиме. Пока просто запустите предложенный код, подробно мы его разберем чуть позже.
Пример 2.2
Сюрприз! Мгновенно выполнились быстрые части обеих функций и затем через 3 секунды (3, а не 6!) одновременно появились оба текстовых сообщения. Полное ощущение, что функции выполнились параллельно (на самом деле нет).
А можно аналогичным образом добавить еще одну функцию-соню? Пожалуйста — хоть сто! Общее время выполнения программы будет по-прежнему определяться самой «тормознутой» из них. Добро пожаловать в асинхронный мир!
Что изменилось в коде?
Перед определениями функций появился префикс async . Он говорит интерпретатору, что функция должна выполняться асинхронно.
Вместо привычного time.sleep мы использовали asyncio.sleep . Это «неблокирующий sleep». В рамках функции ведет себя так же, как традиционный, но не останавливает интерпретатор в целом.
Перед вызовом асинхронных функций появился префикс await . Он говорит интерпретатору примерно следующее: «я тут возможно немного потуплю, но ты меня не жди — пусть выполняется другой код, а когда у меня будет настроение продолжиться, я тебе маякну».
На базе функций мы при помощи asyncio.create_task создали задачи (что это такое разберем позже) и запустили все это при помощи asyncio.run
Как это работает:
выполнилась быстрая часть функции fun1
fun1 сказала интерпретатору «иди дальше, я посплю 3 секунды»
выполнилась быстрая часть функции fun2
fun2 сказала интерпретатору «иди дальше, я посплю 3 секунды»
интерпретатору дальше делать нечего, поэтому он ждет пока ему маякнет первая проснувшаяся функция
на доли миллисекунды раньше проснулась fun1 (она ведь и уснула чуть раньше) и отрапортовала нам об успешном завершении
то же самое сделала функция fun2
Замените «посплю» на «пошлю запрос удаленному сервису и буду ждать ответа» и вы поймете как работает реальное асинхронное приложение.
Возможно в других руководствах вам встретится «старомодный» код типа:
Пример 2.3
Результат тот же самый, но появилось упоминание о каком-то непонятном цикле событий (event loop) и вместо одной asyncio.run используются аж три функции: asyncio.wait , asyncio.get_event_loop и asyncio.run_until_complete . Кроме того, если вы используете python версии 3.10+, в консоль прилетает раздражающее предупреждение DeprecationWarning: There is no current event loop , что само по себе наводит на мысль, что мы делаем что-то слегка не так.
Давайте пока руководствоваться Дзен питона: «Простое лучше, чем сложное», а цикл событий сам придет к нам. в свое время.
Пара слов о «медленных» операциях
Как правило, это все, что связано с вводом выводом: получение результата http-запроса, файловые операции, обращение к базе данных.
Однако следует четко понимать: для эффективного использования с asyncio любой медленный интерфейс должен поддерживать асинхронные функции. Иначе никакого выигрыша в производительности вы не получите. Попробуйте использовать в примере 2.2 time.sleep вместо asyncio.sleep и вы поймете о чем я.
Что касается http-запросов, то здесь есть великолепная библиотека aiohttp , честно реализующая асинхронный доступ к веб-серверу. С файловыми операциями сложнее. В Linux доступ к файловой системе по определению не асинхронный, поэтому, несмотря на наличие удобной библиотеки aiofiles , где-то в ее глубине всегда будет иметь место многопоточный «мостик» к низкоуровневым функциям ОС. С доступом к БД примерно то же самое. Вроде бы, последние версии SQLAlchemy поддерживают асинхронный доступ, но что-то мне подсказывает, что в основе там все тот же старый добрый Threadpool. С другой стороны, в веб-приложениях львиная доля задержек относится именно к сетевому общению, так что «не вполне асинхронный» доступ к локальным ресурсам обычно не является бутылочным горлышком.
Внимательные читатели меня поправили в комментах. В Linux, начиная с ядра 5.1, есть полноценный асинхронный интерфейс io_uring и это прекрасно. Кому интересны детали, рекомендую пройти вот сюда.
3. Асинхронные функции и корутины
Теперь давайте немного разберемся с типами. Вернемся к «неасинхронному» примеру 2.1, слегка модифицировав его:
Пример 3.1
Все вполне ожидаемо. Функция имеет тип <class ‘function’> , а ее результат — <class ‘NoneType’>
Теперь аналогичным образом исследуем «асинхронный» пример 2.2:
Пример 3.2
Уже интереснее! Класс функции не изменился, но благодаря ключевому слову async она теперь возвращает не <class ‘NoneType’> , а <class ‘coroutine’> . Ничто превратилось в нечто! На сцену выходит новая сущность — корутина.
Что нам нужно знать о корутине? На начальном этапе немного. Помните как в python устроен генератор? Ну, это то, что функция начинает возвращать, если в нее добавить yield вместо return . Так вот, корутина — это разновидность генератора.
Корутина дает интерпретатору возможность возобновить базовую функцию, которая была приостановлена в месте размещения ключевого слова await .
И вот тут начинается терминологическая путаница, которая попила немало крови добрых разработчиков на собеседованиях. Сплошь и рядом корутиной называют саму функцию, содержащую await . Строго говоря, это неправильно. Корутина — это то, что возвращает функция с await . Чувствуете разницу между f и f() ?
С генераторами, кстати, та же самая история. Генератором как-то повелось называть функцию, содержащую yield , хотя по правильному-то она «генераторная функция». А генератор — это именно тот объект, который генераторная функция возвращает.
Далее по тексту мы постараемся придерживаться правильной терминологии: асинхронная (или корутинная) функция — это f , а корутина — f() . Но если вы в разговоре назовете корутиной асинхронную функцию, беды большой не произойдет, вас поймут. «Не важно, какого цвета кошка, лишь бы она ловила мышей» (с) тов. Дэн Сяопин
4. Футуры и задачи
Продолжим исследовать нашу программу из примера 2.2. Помнится, на базе корутин мы там создали какие-то загадочные задачи:
Пример 4.1
Ага, значит задача (что бы это ни значило) имеет тип <class ‘_asyncio.Task’> . Привет, капитан Очевидность!
А кто ваша мама, ребята? А мама наша —
анархиякакая-то еще более загадочная футура ( <class ‘_asyncio.Future’> ).В asyncio все шиворот-навыворот, поэтому сначала выясним что такое футура (которую мы видим впервые в жизни), а потом разберемся с ее дочкой задачей (с которой мы уже имели честь познакомиться в предыдущем разделе).
Футура (если совсем упрощенно) — это оболочка для некой асинхронной сущности, позволяющая выполнять ее «как бы одновременно» с другими асинхронными сущностями, переключаясь от одной сущности к другой в точках, обозначенных ключевым словом await
Кроме того футура имеет внутреннюю переменную «результат», которая доступна через .result() и устанавливается через .set_result(value) . Пока ничего не надо делать с этим знанием, оно пригодится в дальнейшем.
У футуры на самом деле еще много чего есть внутри, но на данном этапе не будем слишком углубляться. Футуры в чистом виде используются в основном разработчиками фреймворков, нам же для разработки приложений приходится иметь дело с их дочками — задачами.
Задача — это частный случай футуры, предназначенный для оборачивания корутины.
Все трагически усложняется
Вернемся к примеру 2.2 и опишем его логику заново, используя теперь уже знакомые нам термины — корутины и задачи:
корутину асинхронной функции fun1 обернули задачей task1
корутину асинхронной функции fun2 обернули задачей task2
в асинхронной функции main обозначили точку переключения к задаче task1
в асинхронной функции main обозначили точку переключения к задаче task2
корутину асинхронной функции main передали в функцию asyncio.run
Бр-р-р, ужас какой. Воистину: «Во многой мудрости много печали; и кто умножает познания, умножает скорбь» (Еккл. 1:18)
Все счастливо упрощается
А можно проще? Ведь понятие корутина нам необходимо, только чтобы отличать функцию от результата ее выполнения. Давайте попробуем временно забыть про них. Попробуем также перефразировать неуклюжие «точки переключения» и вот эти вот все «обернули-передали». Кроме того, поскольку asyncio.run — это единственная рекомендованная точка входа в приложение для python 3.8+, ее отдельное упоминание тоже совершенно излишне для понимания логики нашего приложения.
А теперь (барабанная дробь). Мы вообще уберем из кода все упоминания об асинхронности. Я понимаю, что работать не будет, но все же давайте посмотрим что получится:
Пример 4.2 (не работающий)
Кощунство, скажете вы? Нет, я всего лишь честно выполняю рекомендацию великого и ужасного Гвидо ван Россума:
«Прищурьтесь и притворитесь, что ключевых слов async и await нет»
Звучит почти как: «Наденьте зеленые очки и притворитесь, что стекляшки — это изумруды»
Итак, в «прищуренной вселенной Гвидо»:
Задачи — это «ракеты-носители» для конкурентного запуска «боеголовок»-функций.
А если вообще без задач?
Как это? Ну вот так, ни в какие задачи ничего не заворачивать, а просто эвейтнуть в main() сами корутины. А что, имеем право!
Пример 4.3 (неудачный)
Грусть-печаль. Снова 6 секунд как в давнем примере 1.1, ни разу не асинхронном. Боеголовка без ракеты взлетать отказалась.
Вывод:
В asyncio.run нужно передавать асинхронную функцию с эвейтами на задачи, а не на корутины. Иначе не взлетит. То есть работать-то будет, но сугубо последовательно, без всякой конкурентности.
Пара слов о конкурентности
С точки зрения разработчика и (особенно) пользователя конкурентное выполнение в асинхронных и многопоточных приложениях выглядит почти как параллельное. На самом деле никакого параллельного выполнения чего бы то ни было в питоне нет и быть не может. Кто не верит — погулите аббревиатуру GIL. Именно поэтому мы используем осторожное выражение «конкурентное выполнение задач» вместо «параллельное».
Нет, конечно, если очень хочется настоящего параллелизма, можно запустить несколько интерпретаторов python одновременно (библиотека multiprocessing фактически так и делает). Но без крайней нужды лучше такими вещами не заниматься, ибо издержки чаще всего будут непропорционально велики по сравнению с профитом.
А что есть «крайняя нужда»? Это приложения-числодробилки. В них подавляющая часть времени выполнения расходуется на операции процессора и обращения к памяти. Никакого ленивого ожидания ответа от медленной периферии, только жесткий математический хардкор. В этом случае вас, конечно, не спасет ни изящная асинхронность, ни неуклюжая мультипоточность. К счастью, такие негуманные приложения в практике веб-разработки встречаются нечасто.
5. Асинхронные менеджеры контекста и настоящее асинхронное приложение
Пришло время написать на asyncio не тупой перебор неблокирующих слипов, а что-то выполняющее действительно осмысленную работу. Но прежде чем приступить, разберемся с асинхронными менеджерами контекста.
Если вы умеете работать с обычными менеджерами контекста, то без труда освоите и асинхронные. Тут используется знакомая конструкция with , только с префиксом async , и те же самые контекстные методы, только с буквой a в начале.
Пример 5.1
Создавать свои асинхронные менеджеры контекста разработчику приложений приходится нечасто, а вот использовать готовые из асинхронных библиотек — постоянно. Поэтому нам полезно знать, что находится у них внутри.
Теперь, зная как работают асинхронные менеджеры контекста, можно написать ну очень полезное приложение, которое узнает погоду в разных городах при помощи библиотеки aiohttp и API-сервиса openweathermap.org:
Пример 5.2
«И говорит по радио товарищ Левитан: в Москве погода ясная, а в Лондоне — туман!» (c) Е.Соев
Кстати, ключик к API дарю, пользуйтесь на здоровье.
Внимание! Если будет слишком много желающих потестить сервис с моим ключом, его могут временно заблокировать. В этом случае просто получите свой собственный, это быстро и бесплатно.
Опрос 12-ти городов на моем канале 100Mb занимает доли секунды.
Обратите внимание, мы использовали два вложенных менеджера контекста: для сессии и для функции get . Так требует документация aiohttp , не будем с ней спорить.
Давайте попробуем реализовать тот же самый функционал, используя классическую синхронную библиотеку requests и сравним скорость:
Пример 5.3
Работает превосходно, но. В среднем занимает 2-3 секунды, то есть раз в 10 больше чем в асинхронном примере. Что и требовалось доказать.
А может ли асинхронная функция не просто что-то делать внутри себя (например, запрашивать и выводить в консоль погоду), но и возвращать результат? Ту же погоду, например, чтобы дальнейшей обработкой занималась функция верхнего уровня main() .
Нет ничего проще. Только в этом случае для группового запуска задач необходимо использовать уже не цикл с await , а функцию asyncio.gather
Пример 5.4
Красиво получилось! Обратите внимание, мы использовали выражение со звездочкой *tasks для распаковки списка задач в аргументы функции asyncio.gather .
Пара слов о лишних сущностях
Кажется, я совершил невозможное. Настучал уже почти тысячу строк текста и ни разу не упомянул о цикле событий. Ну, почти ни разу. Один раз все-же упомянул: в примере 2.3 «как не надо делать». А между тем, в традиционных руководствах по asyncio этим самым циклом событий начинают душить несчастного читателя буквально с первой страницы. На самом деле цикл событий в наших программах присутствует, но он надежно скрыт от посторонних глаз высокоуровневыми конструкциями. До сих пор у нас не возникало в нем нужды, вот и я и не стал плодить лишних сущностей, руководствуясь принципом дорогого товарища Оккама.
Но рано или поздно жизнь заставит нас извлечь этот скелет из шкафа и рассмотреть его во всех подробностях.
- A coroutine object, which is the return value of a coroutine function defined using async def .