a-conceptual-overview-of-asyncio.md
1> **Источник:** https://python-all.ru/3/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/howto/index.html#how-tos) – помочь построить прочную мысленную модель того, как [`asyncio`](https://python-all.ru/3/library/asyncio.html#module-asyncio) работает по своей сути, чтобы понять как и почему работают рекомендуемые подходы.1011Может возникнуть интерес к некоторым ключевым понятиям `asyncio`. К концу статьи можно будет с уверенностью ответить на следующие вопросы:1213- Что происходит за кулисами, когда ожидается объект?14- Как `asyncio` различает задачу, которой не нужно процессорное время (например, сетевой запрос или чтение файла), и задачу, которой оно нужно (например, вычисление факториала n)?15- Как написать асинхронный вариант операции, такой как async sleep или запрос к базе данных.1617> **См. также**18>19> - [Руководство](https://python-all.ru/3/howto/a-conceptual-overview-of-asyncio.html), вдохновившее эту статью HOWTO, автор Александр Нордин.20> - Этот подробный [серия учебных видео на YouTube](https://python-all.ru/3/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/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/reference/compound_stmts.html#async-def), в отличие от простого `def`, делает эту функцию асинхронной (или «корутинной функцией»). Её вызов создаёт и возвращает объект [корутины](https://python-all.ru/3/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/library/types.html#types.CoroutineType) (нативная корутина). Обратите внимание, что корутины также могут быть экземплярами [`collections.abc.Coroutine`](https://python-all.ru/3/library/collections.abc.html#collections.abc.Coroutine) – это различие важно для проверки типов.8384Корутина представляет тело или логику функции. Корутину необходимо явно запускать; простое создание корутины не запускает её. Важно, что корутину можно приостанавливать и возобновлять в различных точках тела функции. Эта способность приостанавливаться и возобновляться и обеспечивает асинхронное поведение!8586Корутины и корутинные функции были построены с использованием функциональности [генераторов](https://python-all.ru/3/glossary.html#term-generator-iterator) и [генераторных функций](https://python-all.ru/3/glossary.html#term-generator). Напомним, генераторная функция – это функция, которая [`yield`](https://python-all.ru/3/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/library/functions.html#next). Иными словами, генератор выполняется, затем приостанавливается. Например:108109```python110>>> generator = get_random_number()111>>> next(generator)112Hi1131114>>> next(generator)115Hello1167117```118119### Задачи120121Грубо говоря, [задачи](https://python-all.ru/3/library/asyncio-task.html#asyncio-task-obj) – это корутины (не корутинные функции), привязанные к циклу событий. Задача также хранит список колбэк-функций, важность которых станет ясна через мгновение, когда мы обсудим [`await`](https://python-all.ru/3/reference/expressions.html#await). Рекомендуемый способ создания задач – через [`asyncio.create_task()`](https://python-all.ru/3/library/asyncio-task.html#asyncio.create_task).122123Создание задачи автоматически планирует её выполнение (путём добавления колбэка для её запуска в список дел цикла событий, то есть в коллекцию задач).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/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/reference/expressions.html#await) – это ключевое слово Python, которое обычно используется одним из двух способов:169170```python171await task172await coroutine173```174175Важно, что поведение `await` зависит от типа ожидаемого объекта.176177Ожидание задачи передаёт управление от текущей задачи или корутины циклу событий. В процессе передачи управления происходит несколько важных вещей. Для иллюстрации воспользуемся следующим примером кода:178179```python180async def plant_a_tree():181 dig_the_hole_task = asyncio.create_task(dig_the_hole())182 await dig_the_hole_task183184 # Другие инструкции, связанные с посадкой дерева.185 ...186```187188В этом примере представим, что цикл событий передал управление началу корутины `plant_a_tree()`. Как видно выше, корутина создаёт задачу, а затем ожидает её. Инструкция `await dig_the_hole_task` добавляет колбэк (который возобновит `plant_a_tree()`) в список колбэков объекта `dig_the_hole_task`. Затем инструкция передаёт управление циклу событий. Некоторое время спустя цикл событий передаст управление `dig_the_hole_task`, и задача завершит то, что ей нужно. После завершения задачи она добавит свои колбэки в цикл событий, в данном случае – вызов для возобновления `plant_a_tree()`.189190В общем, когда ожидаемая задача завершается (`dig_the_hole_task`), исходная задача или корутина (`plant_a_tree()`) снова добавляется в список дел цикла событий для возобновления.191192Это простая, но надёжная мысленная модель. На практике передача управления немного сложнее, но не намного. В части 2 мы подробно разберём детали, которые делают это возможным.193194**В отличие от задач, ожидание корутины не передаёт управление обратно циклу событий!** Если сначала обернуть корутину в задачу, а затем ожидать её, управление передастся. Поведение `await coroutine` фактически аналогично вызову обычной синхронной функции Python. Рассмотрим такую программу:195196```python197import asyncio198199async def coro_a():200 print("I am coro_a(). Hi!")201202async def coro_b():203 print("I am coro_b(). I sure hope no one hogs the event loop...")204205async def main():206 task_b = asyncio.create_task(coro_b())207 num_repeats = 3208 for _ in range(num_repeats):209 await coro_a()210 await task_b211212asyncio.run(main())213```214215Первый оператор в корутине `main()` создаёт `task_b` и планирует его выполнение через цикл событий. Затем `coro_a()` многократно ожидается. Управление никогда не передаётся циклу событий, поэтому мы видим вывод всех трёх вызовов `coro_a()` перед выводом `coro_b()`:216217```text218I am coro_a(). Hi!219I am coro_a(). Hi!220I am coro_a(). Hi!221I am coro_b(). I sure hope no one hogs the event loop...222```223224Если изменить `await coro_a()` на `await asyncio.create_task(coro_a())`, поведение меняется. Корутина `main()` передаёт управление циклу событий этим оператором. Затем цикл событий проходит по своему накопившемуся списку работ, вызывая `task_b`, а затем задачу, которая обёртывает `coro_a()`, прежде чем возобновить корутину `main()`.225226```text227I am coro_b(). I sure hope no one hogs the event loop...228I am coro_a(). Hi!229I am coro_a(). Hi!230I am coro_a(). Hi!231```232233Такое поведение `await coroutine` может многих сбить с толку! Этот пример показывает, как использование только `await coroutine` может непреднамеренно захватить управление у других задач и фактически заблокировать цикл событий. [`asyncio.run()`](https://python-all.ru/3/library/asyncio-runner.html#asyncio.run) может помочь обнаружить такие случаи с помощью флага `debug=True`, который включает [режим отладки](https://python-all.ru/3/library/asyncio-dev.html#asyncio-debug-mode). Помимо прочего, он будет записывать любые корутины, монополизирующие выполнение на 100 мс и дольше.234235Дизайн намеренно жертвует некоторой концептуальной ясностью в использовании `await` ради повышения производительности. Каждый раз, когда задача ожидается (await), управление должно передаваться вверх по стеку вызовов к циклу событий. Это может звучать незначительно, но в большой программе с множеством операторов `await` и глубоким стеком вызовов эти накладные расходы могут превратиться в ощутимое снижение производительности.236237## Концептуальный обзор, часть 2: практические детали238239Часть 2 подробно рассматривает механизмы, которые `asyncio` использует для управления потоком выполнения. Вот где происходит магия. Из этого раздела вы узнаете, что `await` делает за кулисами и как создавать собственные асинхронные операторы.240241### Внутреннее устройство корутин242243`asyncio` использует четыре компонента для передачи управления.244245[`coroutine.send(arg)`](https://python-all.ru/3/reference/expressions.html#generator.send) – это метод, используемый для запуска или возобновления корутины. Если корутина была приостановлена и теперь возобновляется, аргумент `arg` будет передан как возвращаемое значение оператора `yield`, который изначально её приостановил. Если корутина используется впервые (а не возобновляется), `arg` должно быть `None`.246247```python248class Rock:249 def __await__(self):250 value_sent_in = yield 7251 print(f"Rock.__await__ resuming with value: {value_sent_in}.")252 return value_sent_in253254async def main():255 print("Beginning coroutine main().")256 rock = Rock()257 print("Awaiting rock...")258 value_from_rock = await rock259 print(f"Coroutine received value: {value_from_rock} from rock.")260 return 23261262coroutine = main()263intermediate_result = coroutine.send(None)264print(f"Coroutine paused and returned intermediate value: {intermediate_result}.")265266print(f"Resuming coroutine and sending in value: 42.")267try:268 coroutine.send(42)269except StopIteration as e:270 returned_value = e.value271print(f"Coroutine main() finished and provided value: {returned_value}.")272```273274[yield](https://python-all.ru/3/reference/expressions.html#yieldexpr), как обычно, приостанавливает выполнение и возвращает управление вызывающему. В приведённом выше примере `yield` в строке 3 вызывается `... = await rock` в строке 11. В более широком смысле `await` вызывает метод [`__await__()`](https://python-all.ru/3/reference/datamodel.html#object.__await__) данного объекта. `await` также делает ещё одну очень важную вещь: он распространяет (или «передаёт дальше») любые `yield`, которые получает, вверх по цепочке вызовов. В данном случае – обратно к `... = coroutine.send(None)` в строке 16.275276Корутина возобновляется вызовом `coroutine.send(42)` в строке 21. Корутина продолжает с того места, где она `yield` (или приостановилась) в строке 3, и выполняет оставшиеся операторы своего тела. Когда корутина завершается, она возбуждает исключение [`StopIteration`](https://python-all.ru/3/library/exceptions.html#StopIteration) с возвращаемым значением, прикреплённым в атрибуте [`value`](https://python-all.ru/3/library/exceptions.html#StopIteration.value).277278Этот фрагмент выводит следующий результат:279280```text281Beginning coroutine main().282Awaiting rock...283Coroutine paused and returned intermediate value: 7.284Resuming coroutine and sending in value: 42.285Rock.__await__ resuming with value: 42.286Coroutine received value: 42 from rock.287Coroutine main() finished and provided value: 23.288```289290Стоит на мгновение остановиться и проследить различные способы передачи управления и значений. Было затронуто много важных идей, и важно убедиться, что понимание прочно.291292Единственный способ выполнить yield (или фактически уступить управление) из корутины – `await` объект, который `yield`s в своём методе `__await__`.293294> 1. А что насчёт `yield` непосредственно внутри корутинной функции? Корутинная функция становится [асинхронной функцией-генератором](https://python-all.ru/3/reference/expressions.html#asynchronous-generator-functions), совершенно другой сущностью.295>296> 2. Что насчёт [yield from](https://python-all.ru/3/reference/expressions.html#yieldexpr) внутри корутинной функции для (обычного) генератора? Это вызывает ошибку: `SyntaxError: yield from not allowed in a coroutine.` Это было намеренно сделано для простоты – предписан только один способ использования корутин. Изначально `yield` также был запрещён, но его снова приняли, чтобы разрешить асинхронные генераторы. Несмотря на это, `yield from` и `await` по сути делают одно и то же.297298### Future299300[future](https://python-all.ru/3/library/asyncio-future.html#asyncio-future-obj) – это объект, предназначенный для представления состояния и результата вычисления. Термин отсылает к идее чего-то, что ещё должно произойти или ещё не случилось, и этот объект – способ следить за этим чем-то.301302У future есть несколько важных атрибутов. Один из них – состояние, которое может быть «pending» (ожидание), «cancelled» (отменено) или «done» (завершено). Другой – результат, который устанавливается при переходе состояния в done. В отличие от корутины, future не представляет само вычисление; вместо этого он представляет состояние и результат этого вычисления, что-то вроде индикатора (красный, жёлтый или зелёный) или сигнала.303304[`asyncio.Task`](https://python-all.ru/3/library/asyncio-task.html#asyncio.Task) наследует [`asyncio.Future`](https://python-all.ru/3/library/asyncio-future.html#asyncio.Future), чтобы получить эти различные возможности. В предыдущем разделе говорилось, что задачи хранят список колбэков, что было не совсем точно. На самом деле именно класс `Future` реализует эту логику, которую `Task` наследует.305306Future также можно использовать напрямую (не через задачи). Задачи помечают себя как выполненные при завершении своей корутины. Future гораздо более универсальны: они помечаются как выполненные тогда, когда это указано. Таким образом, они предоставляют гибкий интерфейс для задания собственных условий ожидания и возобновления.307308### Самодельный asyncio.sleep309310Мы рассмотрим пример того, как можно использовать future для создания собственного варианта асинхронного ожидания (`async_sleep`), который имитирует [`asyncio.sleep()`](https://python-all.ru/3/library/asyncio-task.html#asyncio.sleep).311312Этот фрагмент регистрирует несколько задач в цикле событий и затем ожидает задачу, созданную с помощью `asyncio.create_task`, которая оборачивает корутину `async_sleep(3)`. Мы хотим, чтобы эта задача завершилась только через три секунды, но при этом не блокировала выполнение других задач.313314```python315async def other_work():316 print("I like work. Work work.")317318async def main():319 # Добавьте несколько других задач в цикл событий, чтобы было чем заняться320 # во время асинхронного ожидания.321 work_tasks = [322 asyncio.create_task(other_work()),323 asyncio.create_task(other_work()),324 asyncio.create_task(other_work())325 ]326 print(327 "Beginning asynchronous sleep at time: "328 f"{datetime.datetime.now().strftime("%H:%M:%S")}."329 )330 await asyncio.create_task(async_sleep(3))331 print(332 "Done asynchronous sleep at time: "333 f"{datetime.datetime.now().strftime("%H:%M:%S")}."334 )335 # asyncio.gather фактически ожидает каждую задачу в коллекции.336 await asyncio.gather(*work_tasks)337```338339Ниже мы используем future, чтобы обеспечить собственный контроль над тем, когда эта задача будет помечена как выполненная. Если [`future.set_result()`](https://python-all.ru/3/library/asyncio-future.html#asyncio.Future.set_result) (метод, отвечающий за пометку этого future как выполненного) никогда не будет вызван, эта задача никогда не завершится. Мы также привлекли другую задачу, которую увидим через мгновение; она будет отслеживать, сколько времени прошло, и соответственно вызывать `future.set_result()`.340341```python342async def async_sleep(seconds: float):343 future = asyncio.Future()344 time_to_wake = time.time() + seconds345 # Добавьте задачу-наблюдатель в цикл событий.346 watcher_task = asyncio.create_task(_sleep_watcher(future, time_to_wake))347 # Блокируется до тех пор, пока future не будет отмечен как завершённый.348 await future349```350351Ниже мы используем довольно голый объект `YieldToEventLoop()`, чтобы `yield` из его метода `__await__`, передавая управление циклу событий. Это фактически то же самое, что вызов `asyncio.sleep(0)`, но такой подход даёт больше ясности, не говоря уже о том, что использовать `asyncio.sleep` при демонстрации его реализации – это немного мошенничество!352353Как обычно, цикл событий перебирает свои задачи, передавая им управление и получая его обратно, когда они приостанавливаются или завершаются. `watcher_task`, который выполняет корутину `_sleep_watcher(...)`, будет вызываться один раз за полный цикл событий. При каждом возобновлении он проверяет время, и если прошло недостаточно, то снова приостанавливается и возвращает управление циклу событий. Когда нужное время прошло, `_sleep_watcher(...)` помечает future как выполненный и завершается, выходя из своего бесконечного цикла `while`. Поскольку эта вспомогательная задача вызывается только один раз за цикл событий, вы будете правы, заметив, что асинхронное ожидание будет ждать *как минимум* три секунды, а не ровно три. Заметьте, это верно и для `asyncio.sleep`.354355```python356class YieldToEventLoop:357 def __await__(self):358 yield359360async def _sleep_watcher(future, time_to_wake):361 while True:362 if time.time() >= time_to_wake:363 # Это помечает future как завершённый.364 future.set_result(None)365 break366 else:367 await YieldToEventLoop()368```369370Вот вывод полной программы:371372```text373$ python custom-async-sleep.py374Beginning asynchronous sleep at time: 14:52:22.375I like work. Work work.376I like work. Work work.377I like work. Work work.378Done asynchronous sleep at time: 14:52:25.379```380381Возможно, эта реализация асинхронного ожидания кажется излишне запутанной. И, да, так и есть. Пример был призван продемонстрировать универсальность future на простом примере, который можно адаптировать для более сложных задач. Для справки, вот как можно реализовать это без future:382383```python384async def simpler_async_sleep(seconds):385 time_to_wake = time.time() + seconds386 while True:387 if time.time() >= time_to_wake:388 return389 else:390 await YieldToEventLoop()391```392393Но на этом пока всё. Надеемся, теперь вы готовы более уверенно погрузиться в асинхронное программирование или изучить продвинутые темы в [`rest of the documentation`](https://python-all.ru/3/library/asyncio.html#module-asyncio).394