asyncio-dev.md
1> **Источник:** https://python-all.ru/3.15/library/asyncio-dev.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# Разработка с asyncio89Асинхронное программирование отличается от классического «последовательного» программирования.1011На этой странице перечислены типичные ошибки и ловушки, а также объясняется, как их избежать.1213## Режим отладки1415По умолчанию asyncio работает в производственном режиме. Для упрощения разработки в asyncio есть *режим отладки*.1617Включить режим отладки asyncio можно несколькими способами:1819- Установка переменной окружения [`PYTHONASYNCIODEBUG`](https://python-all.ru/3.15/using/cmdline.html#envvar-PYTHONASYNCIODEBUG) в значение `1`.20- Использование [режима разработки Python](https://python-all.ru/3.15/library/devmode.html#devmode).21- Передача `debug=True` в [`asyncio.run()`](https://python-all.ru/3.15/library/asyncio-runner.html#asyncio.run).22- Вызов [`loop.set_debug()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.set_debug).2324В дополнение к включению режима отладки также рекомендуется:2526- Установка уровня журналирования [логгера asyncio](https://python-all.ru/3.15/library/asyncio-dev.html#asyncio-logger) на [`logging.DEBUG`](https://python-all.ru/3.15/library/logging.html#logging.DEBUG); например, следующий фрагмент кода можно выполнить при запуске приложения:2728 ```python29 logging.basicConfig(level=logging.DEBUG)30 ```31- Настройка модуля [`warnings`](https://python-all.ru/3.15/library/warnings.html#module-warnings) на отображение предупреждений [`ResourceWarning`](https://python-all.ru/3.15/library/exceptions.html#ResourceWarning). Один из способов сделать это – использовать параметр командной строки [`-W`](https://python-all.ru/3.15/using/cmdline.html#cmdoption-W) `default`.3233Когда режим отладки включён:3435- Многие небезопасные по отношению к потокам API asyncio (например, методы [`loop.call_soon()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.call_soon) и [`loop.call_at()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.call_at)) вызывают исключение, если их вызывают не из того потока.36- Время выполнения селектора ввода-вывода журналируется, если выполнение операции ввода-вывода занимает слишком много времени.37- Колбэки, выполняющиеся дольше 100 миллисекунд, журналируются. Атрибут [`loop.slow_callback_duration`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.slow_callback_duration) можно использовать для задания минимальной продолжительности выполнения в секундах, которая считается «медленной».3839## Параллелизм и многопоточность4041Цикл событий работает в потоке (обычно в главном) и выполняет все колбэки и задачи в своём потоке. Пока задача выполняется в цикле событий, никакие другие задачи не могут выполняться в том же потоке. Когда задача выполняет выражение `await`, она приостанавливается, и цикл событий переходит к следующей задаче.4243Для планирования [колбэка](https://python-all.ru/3.15/glossary.html#term-callback) из другого потока ОС следует использовать метод [`loop.call_soon_threadsafe()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.call_soon_threadsafe). Пример:4445```python46loop.call_soon_threadsafe(callback, *args)47```4849Почти все объекты asyncio не являются потокобезопасными, что обычно не является проблемой, если только нет кода, который работает с ними вне задачи или колбэка. Если такому коду нужно вызвать низкоуровневый API asyncio, следует использовать метод [`loop.call_soon_threadsafe()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.call_soon_threadsafe), например:5051```python52loop.call_soon_threadsafe(fut.cancel)53```5455Для планирования объекта корутины из другого потока ОС следует использовать функцию [`run_coroutine_threadsafe()`](https://python-all.ru/3.15/library/asyncio-task.html#asyncio.run_coroutine_threadsafe). Она возвращает [`concurrent.futures.Future`](https://python-all.ru/3.15/library/concurrent.futures.html#concurrent.futures.Future) для доступа к результату:5657```python58async def coro_func():59 return await asyncio.sleep(1, 42)6061# Позже в другом потоке ОС:6263future = asyncio.run_coroutine_threadsafe(coro_func(), loop)64# Ожидать результат:65result = future.result()66```6768Для обработки сигналов цикл событий должен выполняться в главном потоке.6970Метод [`loop.run_in_executor()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) можно использовать с [`concurrent.futures.ThreadPoolExecutor`](https://python-all.ru/3.15/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor) или [`InterpreterPoolExecutor`](https://python-all.ru/3.15/library/concurrent.futures.html#concurrent.futures.InterpreterPoolExecutor) для выполнения блокирующего кода в другом потоке ОС, не блокируя поток ОС, в котором работает цикл событий.7172В настоящее время нет возможности планировать корутины или колбэки напрямую из другого процесса (например, запущенного с помощью [`multiprocessing`](https://python-all.ru/3.15/library/multiprocessing.html#module-multiprocessing)). В разделе [Методы цикла событий](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio-event-loop-methods) перечислены API, которые могут читать из каналов и отслеживать файловые дескрипторы, не блокируя цикл событий. Кроме того, API asyncio для [подпроцессов](https://python-all.ru/3.15/library/asyncio-subprocess.html#asyncio-subprocess) позволяют запустить процесс и взаимодействовать с ним из цикла событий. Наконец, упомянутый выше метод [`loop.run_in_executor()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) также можно использовать с [`concurrent.futures.ProcessPoolExecutor`](https://python-all.ru/3.15/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor) для выполнения кода в другом процессе.7374## Выполнение блокирующего кода7576Блокирующий (связанный с процессором) код не следует вызывать напрямую. Например, если функция выполняет интенсивные вычисления в течение 1 секунды, все параллельные задачи asyncio и операции ввода-вывода будут задержаны на 1 секунду.7778Исполнитель можно использовать для выполнения задачи в другом потоке, в том числе в другом интерпретаторе, или даже в другом процессе, чтобы не блокировать поток ОС с циклом событий. Подробнее см. метод [`loop.run_in_executor()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.run_in_executor).7980## Журналирование8182asyncio использует модуль [`logging`](https://python-all.ru/3.15/library/logging.html#module-logging), а все логирование выполняется через логгер `"asyncio"`.8384Уровень логирования по умолчанию – [`logging.INFO`](https://python-all.ru/3.15/library/logging.html#logging.INFO). Его можно легко изменить:8586```python87logging.getLogger("asyncio").setLevel(logging.WARNING)88```8990Сетевое логирование может блокировать цикл событий. Рекомендуется использовать отдельный поток для обработки логов или неблокирующий ввод-вывод. Пример см. в [разделе «Обработчики, которые блокируют выполнение»](https://python-all.ru/3.15/howto/logging-cookbook.html#blocking-handlers).9192## Обнаружение никогда не ожидаемых корутин9394Когда корутинная функция вызывается, но не ожидается (например, `coro()` вместо `await coro()`) или корутина не запланирована с помощью [`asyncio.create_task()`](https://python-all.ru/3.15/library/asyncio-task.html#asyncio.create_task), asyncio выдаёт сообщение [`RuntimeWarning`](https://python-all.ru/3.15/library/exceptions.html#RuntimeWarning):9596```python97import asyncio9899async def test():100 print("never scheduled")101102async def main():103 test()104105asyncio.run(main())106```107108Вывод:109110```python111test.py:7: RuntimeWarning: coroutine 'test' was never awaited112 test()113```114115Вывод в режиме отладки:116117```python118test.py:7: RuntimeWarning: coroutine 'test' was never awaited119Coroutine created at (most recent call last)120 File "../t.py", line 9, in <module>121 asyncio.run(main(), debug=True)122123 < .. >124125 File "../t.py", line 7, in main126 test()127 test()128```129130Обычно это исправляется либо ожиданием корутины, либо вызовом функции [`asyncio.create_task()`](https://python-all.ru/3.15/library/asyncio-task.html#asyncio.create_task):131132```python133async def main():134 await test()135```136137## Обнаружение никогда не извлечённых исключений138139Если вызван метод [`Future.set_exception()`](https://python-all.ru/3.15/library/asyncio-future.html#asyncio.Future.set_exception), но сам объект Future никогда не ожидается, исключение не будет передано пользовательскому коду. В этом случае asyncio выдаёт сообщение в журнал при сборке мусора для этого Future.140141Пример необработанного исключения:142143```python144import asyncio145146async def bug():147 raise Exception("not consumed")148149async def main():150 asyncio.create_task(bug())151152asyncio.run(main())153```154155Вывод:156157```python158Task exception was never retrieved159future: <Task finished coro=<bug() done, defined at test.py:3>160 exception=Exception('not consumed')>161162Traceback (most recent call last):163 File "test.py", line 4, in bug164 raise Exception("not consumed")165Exception: not consumed166```167168[Включите режим отладки](https://python-all.ru/3.15/library/asyncio-dev.html#asyncio-debug-mode), чтобы получить трассировку стека на момент создания задачи:169170```python171asyncio.run(main(), debug=True)172```173174Вывод в режиме отладки:175176```python177Task exception was never retrieved178future: <Task finished coro=<bug() done, defined at test.py:3>179 exception=Exception('not consumed') created at asyncio/tasks.py:321>180181source_traceback: Object created at (most recent call last):182 File "../t.py", line 9, in <module>183 asyncio.run(main(), debug=True)184185< .. >186187Traceback (most recent call last):188 File "../t.py", line 4, in bug189 raise Exception("not consumed")190Exception: not consumed191```192193## Рекомендации по работе с асинхронными генераторами194195Чтобы писать правильный и эффективный код на asyncio, нужно знать о некоторых подводных камнях. В этом разделе собраны основные рекомендации, которые могут сэкономить часы отладки.196197### Явное закрытие асинхронных генераторов198199Рекомендуется вручную закрывать [асинхронный генератор](https://python-all.ru/3.15/glossary.html#term-asynchronous-generator-iterator). Если генератор завершается досрочно – например, из-за исключения, возникшего в теле цикла `async for`, – его асинхронный код очистки может выполниться в неожиданном контексте. Это может произойти уже после завершения задач, от которых он зависит, или при остановке цикла событий, когда срабатывает сборщик мусора для async-генератора.200201Чтобы этого избежать, явно закрывайте генератор, вызывая его метод [`aclose()`](https://python-all.ru/3.15/reference/expressions.html#agen.aclose), или используйте контекстный менеджер [`contextlib.aclosing()`](https://python-all.ru/3.15/library/contextlib.html#contextlib.aclosing):202203```python204import asyncio205import contextlib206207async def gen():208 yield 1209 yield 2210211async def func():212 async with contextlib.aclosing(gen()) as g:213 async for x in g:214 break # Не выполнять итерацию до конца215216asyncio.run(func())217```218219Как уже говорилось, код очистки таких асинхронных генераторов откладывается. Следующий пример показывает, что финализация асинхронного генератора может произойти в неожиданном порядке:220221```python222import asyncio223work_done = False224225async def cursor():226 try:227 yield 1228 finally:229 assert work_done230231async def rows():232 global work_done233 try:234 yield 2235 finally:236 await asyncio.sleep(0.1) # имитировать асинхронную работу237 work_done = True238239async def main():240 async for c in cursor():241 async for r in rows():242 break243 break244245asyncio.run(main())246```247248Для этого примера получаем следующий вывод:249250```python251unhandled exception during asyncio.run() shutdown252task: <Task finished name='Task-3' coro=<<async_generator_athrow without __name__>()> exception=AssertionError()>253Traceback (most recent call last):254 File "example.py", line 6, in cursor255 yield 1256asyncio.exceptions.CancelledError257258During handling of the above exception, another exception occurred:259260Traceback (most recent call last):261 File "example.py", line 8, in cursor262 assert work_done263 ^^^^^^^^^264AssertionError265```266267Асинхронный генератор `cursor()` был финализирован до генератора `rows` – неожиданное поведение.268269Пример можно исправить, явно закрыв async-генераторы `cursor` и `rows`:270271```python272async def main():273 async with contextlib.aclosing(cursor()) as cursor_gen:274 async for c in cursor_gen:275 async with contextlib.aclosing(rows()) as rows_gen:276 async for r in rows_gen:277 break278 break279```280281### Создавайте асинхронные генераторы только при работающем цикле событий282283Рекомендуется создавать [асинхронные генераторы](https://python-all.ru/3.15/glossary.html#term-asynchronous-generator-iterator) только после того, как цикл событий уже создан.284285Чтобы гарантировать надёжное закрытие асинхронных генераторов, цикл событий использует функцию [`sys.set_asyncgen_hooks()`](https://python-all.ru/3.15/library/sys.html#sys.set_asyncgen_hooks) для регистрации колбэков. Эти колбэки обновляют список запущенных асинхронных генераторов, поддерживая его в согласованном состоянии.286287При вызове функции [`loop.shutdown_asyncgens()`](https://python-all.ru/3.15/library/asyncio-eventloop.html#asyncio.loop.shutdown_asyncgens) запущенные генераторы корректно останавливаются, а список очищается.288289Асинхронный генератор вызывает соответствующий системный хук при первой итерации. Одновременно генератор запоминает, что хук уже был вызван, и больше не вызывает его.290291Поэтому, если итерация начинается до создания цикла событий, цикл событий не сможет добавить генератор в свой список активных генераторов – хуки устанавливаются уже после того, как генератор попытался их вызвать. В результате цикл событий не сможет при необходимости завершить такой генератор.292293Рассмотрим следующий пример:294295```python296import asyncio297298async def agenfn():299 try:300 yield 10301 finally:302 await asyncio.sleep(0)303304with asyncio.Runner() as runner:305 agen = agenfn()306 print(runner.run(anext(agen)))307 del agen308```309310Вывод:311312```python31310314Exception ignored while closing generator <async_generator object agenfn at 0x000002F71CD10D70>:315Traceback (most recent call last):316 File "example.py", line 13, in <module>317 del agen318 ^^^^319RuntimeError: async generator ignored GeneratorExit320```321322Этот пример можно исправить так:323324```python325import asyncio326327async def agenfn():328 try:329 yield 10330 finally:331 await asyncio.sleep(0)332333async def main():334 agen = agenfn()335 print(await anext(agen))336 del agen337338asyncio.run(main())339```340341### Избегайте одновременных итерации и закрытия одного и того же генератора342343Асинхронные генераторы могут быть повторно вызваны во время выполнения другого вызова [`__anext__()`](https://python-all.ru/3.15/reference/expressions.html#agen.__anext__) / [`athrow()`](https://python-all.ru/3.15/reference/expressions.html#agen.athrow) / [`aclose()`](https://python-all.ru/3.15/reference/expressions.html#agen.aclose). Это может привести к несогласованному состоянию async-генератора и вызвать ошибки.344345Рассмотрим следующий пример:346347```python348import asyncio349350async def consumer():351 for idx in range(100):352 await asyncio.sleep(0)353 message = yield idx354 print('received', message)355356async def amain():357 agenerator = consumer()358 await agenerator.asend(None)359360 fa = asyncio.create_task(agenerator.asend('A'))361 fb = asyncio.create_task(agenerator.asend('B'))362 await fa363 await fb364365asyncio.run(amain())366```367368Вывод:369370```python371received A372Traceback (most recent call last):373 File "test.py", line 38, in <module>374 asyncio.run(amain())375 ~~~~~~~~~~~^^^^^^^^^376 File "Lib/asyncio/runners.py", line 204, in run377 return runner.run(main)378 ~~~~~~~~~~^^^^^^379 File "Lib/asyncio/runners.py", line 127, in run380 return self._loop.run_until_complete(task)381 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^382 File "Lib/asyncio/base_events.py", line 719, in run_until_complete383 return future.result()384 ~~~~~~~~~~~~~^^385 File "test.py", line 36, in amain386 await fb387RuntimeError: anext(): asynchronous generator is already running388```389390Поэтому рекомендуется избегать использования асинхронных генераторов в параллельных задачах или в разных циклах событий.391