a-conceptual-overview-of-asyncio.md
1> **Источник:** https://python-all.ru/3.16/howto/a-conceptual-overview-of-asyncio.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# Концептуальный обзор `asyncio`89Цель этой статьи [HOWTO](https://python-all.ru/3.16/howto/index.html#how-tos) – помочь построить прочную мысленную модель того, как [`asyncio`](https://python-all.ru/3.16/library/asyncio.html#module-asyncio) работает по своей сути, чтобы понять как и почему работают рекомендуемые подходы.1011Может возникнуть интерес к некоторым ключевым понятиям `asyncio`. К концу статьи можно будет с уверенностью ответить на следующие вопросы:1213- Что происходит за кулисами, когда ожидается объект?14- Как `asyncio` различает задачу, которой не нужно процессорное время (например, сетевой запрос или чтение файла), и задачу, которой оно нужно (например, вычисление факториала n)?15- Как написать асинхронный вариант операции, такой как async sleep или запрос к базе данных.1617> **См. также**18>19> - [Руководство](https://python-all.ru/3.16/howto/a-conceptual-overview-of-asyncio.html), вдохновившее эту статью HOWTO, автор Александр Нордин.20> - Этот подробный [серия учебных видео на YouTube](https://python-all.ru/3.16/howto/a-conceptual-overview-of-asyncio.html) по `asyncio`, созданный участником основной команды Python, Лукашем Лангой.21> - [500 Lines or Less: A Web Crawler With asyncio Coroutines](https://python-all.ru/3.16/howto/a-conceptual-overview-of-asyncio.html), авторы А. Джесси Джирю Дэвис и Гвидо ван Россум.2223## Концептуальный обзор, часть 1: высокоуровневый2425В части 1 мы рассмотрим основные высокоуровневые строительные блоки `asyncio`: цикл событий, корутинные функции, объекты корутин, задачи и `await`.2627### Цикл событий2829Всё в `asyncio` происходит относительно цикла событий. Это главная звезда, но предпочитает работать за кулисами, управляя и координируя ресурсы. Он как дирижёр оркестра. Некоторые полномочия ему явно делегированы, но большая часть его способности добиваться результата основана на уважении и сотрудничестве участников оркестра.3031В более технических терминах, цикл событий содержит набор задач для выполнения. Некоторые задачи добавляются напрямую пользователем, а некоторые косвенно – через `asyncio`. Цикл событий берёт задачу из своего бэклога и запускает её (или «передаёт ей управление»), аналогично вызову функции, после чего задача выполняется. Как только она приостанавливается или завершается, управление возвращается циклу событий. Затем цикл событий выбирает другую задачу из своего пула и запускает её. Можно *примерно* представить набор задач как очередь: задачи добавляются, а затем обрабатываются по одной, обычно (но не всегда) по порядку. Этот процесс повторяется бесконечно: цикл событий крутится безостановочно. Если задач, ожидающих выполнения, больше нет, цикл событий достаточно умён, чтобы отдохнуть и не тратить процессорные циклы впустую, и возобновит работу, когда появятся новые задачи, например, когда завершатся операции ввода-вывода или истечёт таймер.3233Эффективное выполнение зависит от того, насколько хорошо задачи делятся ресурсами и сотрудничают; жадная задача может захватить управление и оставить другие задачи голодать, что сделает подход с циклом событий бесполезным.3435```python36import asyncio3738# Это создаёт цикл событий и бесконечно перебирает39# свою коллекцию задач.40event_loop = asyncio.new_event_loop()41event_loop.run_forever()42```4344### Асинхронные функции и корутины4546Это простая, скучная функция Python:4748```python49def hello_printer():50 print(51 "Hi, I am a lowly, simple printer, though I have all I "52 "need in life -- \nfresh paper and my dearly beloved octopus "53 "partner in crime."54 )55```5657Вызов обычной функции запускает её логику или тело:5859```python60>>> hello_printer()61Hi, I am a lowly, simple printer, though I have all I need in life --62fresh paper and my dearly beloved octopus partner in crime.63```6465Конструкция [async def](https://python-all.ru/3.16/reference/compound_stmts.html#async-def), в отличие от простого `def`, делает эту функцию асинхронной (или «корутинной функцией»). Её вызов создаёт и возвращает объект [корутины](https://python-all.ru/3.16/library/asyncio-task.html#coroutine).6667```python68async def loudmouth_penguin(magic_number: int):69 print(70 "I am a super special talking penguin. Far cooler than that printer. "71 f"By the way, my lucky number is: {magic_number}."72 )73```7475Вызов асинхронной функции `loudmouth_penguin` не выполняет оператор print; вместо этого он создаёт объект корутины:7677```python78>>> loudmouth_penguin(magic_number=3)79<coroutine object loudmouth_penguin at 0x104ed2740>80```8182Термины «корутинная функция» и «объект корутины» часто путают, называя и то, и другое просто корутиной. Это может сбивать с толку! В этой статье под корутиной подразумевается именно объект корутины, или, более точно, экземпляр [`types.CoroutineType`](https://python-all.ru/3.16/library/types.html#types.CoroutineType) (нативная корутина). Обратите внимание, что корутины также могут быть экземплярами [`collections.abc.Coroutine`](https://python-all.ru/3.16/library/collections.abc.html#collections.abc.Coroutine) – это различие важно для проверки типов.8384Корутина представляет тело или логику функции. Корутину необходимо явно запускать; простое создание корутины не запускает её. Важно, что корутину можно приостанавливать и возобновлять в различных точках тела функции. Эта способность приостанавливаться и возобновляться и обеспечивает асинхронное поведение!8586Корутины и корутинные функции были построены с использованием функциональности [генераторов](https://python-all.ru/3.16/glossary.html#term-generator-iterator) и [генераторных функций](https://python-all.ru/3.16/glossary.html#term-generator). Напомним, генераторная функция – это функция, которая [`yield`](https://python-all.ru/3.16/reference/simple_stmts.html#yield), например:8788```python89def get_random_number():90 # Это был бы плохой генератор случайных чисел!91 print("Hi")92 yield 193 print("Hello")94 yield 795 print("Howdy")96 yield 497 ...98```99100Как и корутинная функция, вызов генераторной функции не запускает её. Вместо этого создаётся объект-генератор:101102```python103>>> get_random_number()104<generator object get_random_number at 0x1048671c0>105```106107Перейти к следующему `yield` генератора можно с помощью встроенной функции [`next()`](https://python-all.ru/3.16/library/functions.html#next). Иными словами, генератор выполняется, затем приостанавливается. Например:108109```python110>>> generator = get_random_number()111>>> next(generator)112Hi1131114>>> next(generator)115Hello1167117```118119### Задачи120121Грубо говоря, [задачи](https://python-all.ru/3.16/library/asyncio-task.html#asyncio-task-obj) – это корутины (не корутинные функции), привязанные к циклу событий. Задача также поддерживает список колбэк-функций, важность которых станет ясна через минуту, когда мы обсудим [`await`](https://python-all.ru/3.16/reference/expressions.html#await).122123Создание задачи автоматически планирует её выполнение (путём добавления колбэка для её запуска в список дел цикла событий, то есть в набор работ). Рекомендуемый способ создания задач – через [`asyncio.create_task()`](https://python-all.ru/3.16/library/asyncio-task.html#asyncio.create_task).124125`asyncio` автоматически связывает задачи с циклом событий. Эта автоматическая связь была намеренно встроена в `asyncio` для простоты. Без неё пришлось бы отслеживать объект цикла событий и передавать его любой корутинной функции, которая хочет создавать задачи, добавляя избыточный мусор в код.126127```python128coroutine = loudmouth_penguin(magic_number=5)129# Это создаёт объект задачи и планирует его выполнение через цикл событий.130task = asyncio.create_task(coroutine)131```132133Ранее мы вручную создавали цикл событий и настраивали его на вечную работу. На практике рекомендуется использовать (и часто встречается) [`asyncio.run()`](https://python-all.ru/3.16/library/asyncio-runner.html#asyncio.run), который берёт на себя управление циклом событий и гарантирует, что переданная корутина завершится перед продолжением. Например, многие асинхронные программы следуют такой схеме:134135```python136import asyncio137138async def main():139 # Выполняются всевозможные безумные асинхронные действия...140 ...141142if __name__ == "__main__":143 asyncio.run(main())144 # Программа не дойдёт до следующего оператора print, пока145 # корутина main() не завершится.146 print("coroutine main() is done!")147```148149Важно понимать, что сама задача не добавляется в цикл событий – добавляется только колбэк к задаче. Это важно, если созданный объект задачи будет собран сборщиком мусора до того, как цикл событий вызовет его. Например, рассмотрим такую программу:150151```python152async def hello():153 print("hello!")154155async def main():156 asyncio.create_task(hello())157 # Другие асинхронные инструкции, которые выполняются некоторое время158 # и передают управление циклу событий...159 ...160161asyncio.run(main())162```163164Поскольку на объект задачи, созданной в строке 5, нет ссылки, он *может* быть собран сборщиком мусора до того, как цикл событий вызовет его. Последующие инструкции в корутине `main()` передают управление обратно циклу событий, чтобы он мог выполнить другие работы. Когда цикл событий в конце концов попытается запустить задачу, он может обнаружить, что объекта задачи не существует! Это может произойти, даже если корутина хранит ссылку на задачу, но завершается до того, как задача закончится. При выходе из корутины локальные переменные выходят из области видимости и могут быть подвергнуты сборке мусора. На практике `asyncio` и сборщик мусора Python очень стараются, чтобы этого не произошло. Но это не повод быть безрассудным!165166### await167168[`await`](https://python-all.ru/3.16/reference/expressions.html#await) – это ключевое слово Python, которое обычно используется одним из двух способов:169170```python171await task172await coroutine173```174175Важно, что поведение `await` зависит от типа ожидаемого объекта.176177#### Ожидание задач178179Ожидание задачи передаёт управление от текущей задачи или корутины циклу событий. В процессе передачи управления происходит несколько важных вещей. Для иллюстрации воспользуемся следующим примером кода:180181```python182async def plant_a_tree():183 dig_the_hole_task = asyncio.create_task(dig_the_hole())184 await dig_the_hole_task185186 # Другие инструкции, связанные с посадкой дерева.187 ...188```189190В этом примере представим, что цикл событий передал управление началу корутины `plant_a_tree()`. Как видно выше, корутина создаёт задачу, а затем ожидает её. Инструкция `await dig_the_hole_task` добавляет колбэк (который возобновит `plant_a_tree()`) в список колбэков объекта `dig_the_hole_task`. Затем инструкция передаёт управление циклу событий. Некоторое время спустя цикл событий передаст управление `dig_the_hole_task`, и задача завершит то, что ей нужно. После завершения задачи она добавит свои колбэки в цикл событий, в данном случае – вызов для возобновления `plant_a_tree()`.191192В общем, когда ожидаемая задача завершается (`dig_the_hole_task`), исходная задача или корутина (`plant_a_tree()`) снова добавляется в список дел цикла событий для возобновления.193194Это простая, но надёжная мысленная модель. На практике передача управления немного сложнее, но не намного. В части 2 мы подробно разберём детали, которые делают это возможным.195196#### Ожидание корутин197198**В отличие от задач, ожидание корутины не передаёт управление обратно циклу событий!** Если сначала обернуть корутину в задачу, а затем ожидать её, управление передастся. Поведение `await coroutine` фактически аналогично вызову обычной синхронной функции Python. Рассмотрим такую программу:199200```python201import asyncio202203async def coro_a():204 print("I am coro_a(). Hi!")205206async def coro_b():207 print("I am coro_b(). I sure hope no one hogs the event loop...")208209async def main():210 task_b = asyncio.create_task(coro_b())211 num_repeats = 3212 for _ in range(num_repeats):213 await coro_a()214 await task_b215216asyncio.run(main())217```218219Первый оператор в корутине `main()` создаёт `task_b` и планирует его выполнение через цикл событий. Затем `coro_a()` многократно ожидается. Управление никогда не передаётся циклу событий, поэтому мы видим вывод всех трёх вызовов `coro_a()` перед выводом `coro_b()`:220221```text222I am coro_a(). Hi!223I am coro_a(). Hi!224I am coro_a(). Hi!225I am coro_b(). I sure hope no one hogs the event loop...226```227228Если изменить `await coro_a()` на `await asyncio.create_task(coro_a())`, поведение меняется. Корутина `main()` передаёт управление циклу событий этим оператором. Затем цикл событий проходит по своему накопившемуся списку работ, вызывая `task_b`, а затем задачу, которая обёртывает `coro_a()`, прежде чем возобновить корутину `main()`.229230```text231I am coro_b(). I sure hope no one hogs the event loop...232I am coro_a(). Hi!233I am coro_a(). Hi!234I am coro_a(). Hi!235```236237Такое поведение `await coroutine` может многих сбить с толку! Этот пример показывает, как использование только `await coroutine` может непреднамеренно захватить управление у других задач и фактически заблокировать цикл событий. [`asyncio.run()`](https://python-all.ru/3.16/library/asyncio-runner.html#asyncio.run) может помочь обнаружить такие случаи с помощью флага `debug=True`, который включает [режим отладки](https://python-all.ru/3.16/library/asyncio-dev.html#asyncio-debug-mode). Помимо прочего, он будет записывать любые корутины, монополизирующие выполнение на 100 мс и дольше.238239Дизайн намеренно жертвует некоторой концептуальной ясностью в использовании `await` ради повышения производительности. Каждый раз, когда ожидается задача, управление должно передаваться вверх по стеку вызовов к циклу событий. Затем цикл событий должен управлять своим внутренним состоянием и выполнять логику обработки, чтобы возобновить следующую работу. Это может показаться мелочью, но в большой программе с множеством `await` эти накладные расходы могут привести к заметному падению производительности.240241## Концептуальный обзор, часть 2: практические детали242243Часть 2 подробно рассматривает механизмы, которые `asyncio` использует для управления потоком выполнения. Вот где происходит магия. Из этого раздела вы узнаете, что `await` делает за кулисами и как создавать собственные асинхронные операторы.244245### Внутреннее устройство корутин246247`asyncio` использует четыре компонента Python для передачи управления.248249[`coroutine.send(arg)`](https://python-all.ru/3.16/reference/expressions.html#generator.send) – это метод, используемый для запуска или возобновления корутины. Если корутина была приостановлена и теперь возобновляется, аргумент `arg` будет передан как возвращаемое значение оператора `yield`, который изначально её приостановил. Если корутина используется впервые (а не возобновляется), `arg` должно быть `None`.250251```python252class Rock:253 def __await__(self):254 value_sent_in = yield 7255 print(f"Rock.__await__ resuming with value: {value_sent_in}.")256 return value_sent_in257258async def main():259 print("Beginning coroutine main().")260 rock = Rock()261 print("Awaiting rock...")262 value_from_rock = await rock263 print(f"Coroutine received value: {value_from_rock} from rock.")264 return 23265266coroutine = main()267intermediate_result = coroutine.send(None)268print(f"Coroutine paused and returned intermediate value: {intermediate_result}.")269270print(f"Resuming coroutine and sending in value: 42.")271try:272 coroutine.send(42)273except StopIteration as e:274 returned_value = e.value275print(f"Coroutine main() finished and provided value: {returned_value}.")276```277278[yield](https://python-all.ru/3.16/reference/expressions.html#yieldexpr), как обычно, приостанавливает выполнение и возвращает управление вызывающему. В приведённом выше примере `yield` в строке 3 вызывается `... = await rock` в строке 11. В более широком смысле `await` вызывает метод [`__await__()`](https://python-all.ru/3.16/reference/datamodel.html#object.__await__) данного объекта. `await` также делает ещё одну очень важную вещь: он распространяет (или «передаёт дальше») любые `yield`, которые получает, вверх по цепочке вызовов. В данном случае – обратно к `... = coroutine.send(None)` в строке 16.279280Корутина возобновляется вызовом `coroutine.send(42)` в строке 21. Корутина продолжает с того места, где она `yield` (или приостановилась) в строке 3, и выполняет оставшиеся операторы своего тела. Когда корутина завершается, она возбуждает исключение [`StopIteration`](https://python-all.ru/3.16/library/exceptions.html#StopIteration) с возвращаемым значением, прикреплённым в атрибуте [`value`](https://python-all.ru/3.16/library/exceptions.html#StopIteration.value).281282Этот фрагмент выводит следующий результат:283284```text285Beginning coroutine main().286Awaiting rock...287Coroutine paused and returned intermediate value: 7.288Resuming coroutine and sending in value: 42.289Rock.__await__ resuming with value: 42.290Coroutine received value: 42 from rock.291Coroutine main() finished and provided value: 23.292```293294Стоит на мгновение остановиться и проследить различные способы передачи управления и значений. Было затронуто много важных идей, и важно убедиться, что понимание прочно.295296Единственный способ выполнить yield (или фактически уступить управление) из корутины – `await` объект, который `yield`s в своём методе `__await__`.297298> 1. А что насчёт `yield` непосредственно внутри корутинной функции? Корутинная функция становится [асинхронной функцией-генератором](https://python-all.ru/3.16/reference/expressions.html#asynchronous-generator-functions), совершенно другой сущностью.299>300> 2. А что насчёт [yield from](https://python-all.ru/3.16/reference/expressions.html#yieldexpr) внутри корутинной функции к (обычному) генератору? Это вызывает ошибку: `SyntaxError: yield from not allowed in a coroutine.`. Это было намеренно сделано для простоты – разрешён только один способ использования корутин. Несмотря на это, `yield from` и `await` фактически делают одно и то же. Изначально `yield` тоже был запрещён, но его снова разрешили для поддержки асинхронных генераторов.301302### Future303304[future](https://python-all.ru/3.16/library/asyncio-future.html#asyncio-future-obj) – это объект, предназначенный для представления состояния и результата вычисления. Термин отсылает к идее чего-то, что ещё должно произойти или ещё не случилось, и этот объект – способ следить за этим чем-то.305306У future есть несколько важных атрибутов. Один из них – состояние, которое может быть «pending» (ожидание), «cancelled» (отменено) или «done» (завершено). Другой – результат, который устанавливается при переходе состояния в done. В отличие от корутины, future не представляет само вычисление; вместо этого он представляет состояние и результат этого вычисления, что-то вроде индикатора (красный, жёлтый или зелёный) или сигнала.307308[`asyncio.Task`](https://python-all.ru/3.16/library/asyncio-task.html#asyncio.Task) наследует [`asyncio.Future`](https://python-all.ru/3.16/library/asyncio-future.html#asyncio.Future), чтобы получить эти различные возможности. В предыдущем разделе говорилось, что задачи хранят список колбэков, что было не совсем точно. На самом деле именно класс `Future` реализует эту логику, которую `Task` наследует.309310Future также можно использовать напрямую (не через задачи). Задачи помечают себя как выполненные при завершении своей корутины. Future гораздо более универсальны: они помечаются как выполненные тогда, когда это указано. Таким образом, они предоставляют гибкий интерфейс для задания собственных условий ожидания и возобновления.311312### Самодельный asyncio.sleep313314Мы рассмотрим пример того, как можно использовать future для создания собственного варианта асинхронного ожидания (`async_sleep`), который имитирует [`asyncio.sleep()`](https://python-all.ru/3.16/library/asyncio-task.html#asyncio.sleep).315316Этот фрагмент регистрирует несколько задач в цикле событий и затем ожидает задачу, созданную с помощью `asyncio.create_task`, которая оборачивает корутину `async_sleep(3)`. Мы хотим, чтобы эта задача завершилась только через три секунды, но при этом не блокировала выполнение других задач.317318```python319async def other_work():320 print("I like work. Work work.")321322async def main():323 # Добавьте несколько других задач в цикл событий, чтобы было чем заняться324 # во время асинхронного ожидания.325 work_tasks = [326 asyncio.create_task(other_work()),327 asyncio.create_task(other_work()),328 asyncio.create_task(other_work())329 ]330 print(331 "Beginning asynchronous sleep at time: "332 f"{datetime.datetime.now().strftime("%H:%M:%S")}."333 )334 await asyncio.create_task(async_sleep(3))335 print(336 "Done asynchronous sleep at time: "337 f"{datetime.datetime.now().strftime("%H:%M:%S")}."338 )339 # asyncio.gather фактически ожидает каждую задачу в коллекции.340 await asyncio.gather(*work_tasks)341```342343Ниже мы используем future, чтобы обеспечить собственный контроль над тем, когда эта задача будет помечена как выполненная. Если [`future.set_result()`](https://python-all.ru/3.16/library/asyncio-future.html#asyncio.Future.set_result) (метод, отвечающий за пометку этого future как выполненного) никогда не будет вызван, эта задача никогда не завершится. Мы также привлекли другую задачу, которую увидим через мгновение; она будет отслеживать, сколько времени прошло, и соответственно вызывать `future.set_result()`.344345```python346async def async_sleep(seconds: float):347 future = asyncio.Future()348 time_to_wake = time.time() + seconds349 # Добавьте задачу-наблюдатель в цикл событий.350 watcher_task = asyncio.create_task(_sleep_watcher(future, time_to_wake))351 # Блокируется до тех пор, пока future не будет отмечен как завершённый.352 await future353```354355Ниже мы используем довольно голый объект `YieldToEventLoop()`, чтобы `yield` из его метода `__await__`, передавая управление циклу событий. Это фактически то же самое, что вызов `asyncio.sleep(0)`, но такой подход даёт больше ясности, не говоря уже о том, что использовать `asyncio.sleep` при демонстрации его реализации – это немного мошенничество!356357Как обычно, цикл событий перебирает свои задачи, передавая им управление и получая его обратно, когда они приостанавливаются или завершаются. `watcher_task`, который выполняет корутину `_sleep_watcher(...)`, будет вызываться один раз за полный цикл событий. При каждом возобновлении он проверяет время, и если прошло недостаточно, то снова приостанавливается и возвращает управление циклу событий. Когда нужное время прошло, `_sleep_watcher(...)` помечает future как выполненный и завершается, выходя из своего бесконечного цикла `while`. Поскольку эта вспомогательная задача вызывается только один раз за цикл событий, вы будете правы, заметив, что асинхронное ожидание будет ждать *как минимум* три секунды, а не ровно три. Заметьте, это верно и для `asyncio.sleep`.358359```python360class YieldToEventLoop:361 def __await__(self):362 yield363364async def _sleep_watcher(future, time_to_wake):365 while True:366 if time.time() >= time_to_wake:367 # Это помечает future как завершённый.368 future.set_result(None)369 break370 else:371 await YieldToEventLoop()372```373374Вот вывод полной программы:375376```text377$ python custom-async-sleep.py378Beginning asynchronous sleep at time: 14:52:22.379I like work. Work work.380I like work. Work work.381I like work. Work work.382Done asynchronous sleep at time: 14:52:25.383```384385Возможно, эта реализация асинхронного ожидания кажется излишне запутанной. И, да, так и есть. Пример был призван продемонстрировать универсальность future на простом примере, который можно адаптировать для более сложных задач. Для справки, вот как можно реализовать это без future:386387```python388async def simpler_async_sleep(seconds):389 time_to_wake = time.time() + seconds390 while True:391 if time.time() >= time_to_wake:392 return393 else:394 await YieldToEventLoop()395```396397Но на этом пока всё. Надеемся, теперь вы готовы более уверенно погрузиться в асинхронное программирование или изучить продвинутые темы в [`rest of the documentation`](https://python-all.ru/3.16/library/asyncio.html#module-asyncio).398