Содержание страницы
Разработка с asyncio¶Developing with asyncio
Асинхронное программирование отличается от классического «последовательного» программирования.
На этой странице перечислены типичные ошибки и ловушки, а также объясняется, как их избежать.
Режим отладки¶Debug Mode
По умолчанию asyncio работает в производственном режиме. Для упрощения разработки в asyncio есть режим отладки.
Включить режим отладки asyncio можно несколькими способами:
Установка переменной окружения
PYTHONASYNCIODEBUGв значение1.Использование режима разработки Python.
Передача
debug=Trueвasyncio.run().Вызов
loop.set_debug().
В дополнение к включению режима отладки также рекомендуется:
Установка уровня журналирования логгера asyncio на
logging.DEBUG; например, следующий фрагмент кода можно выполнить при запуске приложения:logging.basicConfig(level=logging.DEBUG)
Настройка модуля
warningsна отображение предупрежденийResourceWarning. Один из способов сделать это – использовать параметр командной строки-Wdefault.
Когда режим отладки включён:
Многие небезопасные по отношению к потокам API asyncio (например, методы
loop.call_soon()иloop.call_at()) вызывают исключение, если их вызывают не из того потока.Время выполнения селектора ввода-вывода журналируется, если выполнение операции ввода-вывода занимает слишком много времени.
Колбэки, выполняющиеся дольше 100 миллисекунд, журналируются. Атрибут
loop.slow_callback_durationможно использовать для задания минимальной продолжительности выполнения в секундах, которая считается «медленной».
Параллелизм и многопоточность¶Concurrency and Multithreading
Цикл событий работает в потоке (обычно в главном) и выполняет
все колбэки и задачи в своём потоке. Пока задача выполняется в
цикле событий, никакие другие задачи не могут выполняться в том же потоке. Когда задача
выполняет выражение await, она приостанавливается, и
цикл событий переходит к следующей задаче.
Для планирования колбэка из другого потока ОС следует использовать
метод loop.call_soon_threadsafe(). Пример:
loop.call_soon_threadsafe(callback, *args)
Почти все объекты asyncio не являются потокобезопасными, что обычно
не является проблемой, если только нет кода, который работает с ними вне
задачи или колбэка. Если такому коду нужно вызвать
низкоуровневый API asyncio, следует использовать метод
loop.call_soon_threadsafe(), например:
loop.call_soon_threadsafe(fut.cancel)
Для планирования объекта корутины из другого потока ОС следует использовать
функцию run_coroutine_threadsafe(). Она возвращает
concurrent.futures.Future для доступа к результату:
async def coro_func():
return await asyncio.sleep(1, 42)
# Позже в другом потоке ОС:
future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
# Ожидать результат:
result = future.result()
Для обработки сигналов цикл событий должен выполняться в главном потоке.
Метод loop.run_in_executor() можно использовать с
concurrent.futures.ThreadPoolExecutor или
InterpreterPoolExecutor для выполнения
блокирующего кода в другом потоке ОС, не блокируя поток ОС,
в котором работает цикл событий.
В настоящее время нет возможности планировать корутины или колбэки напрямую
из другого процесса (например, запущенного с помощью
multiprocessing). В разделе Методы цикла событий
перечислены API, которые могут читать из каналов и отслеживать файловые дескрипторы,
не блокируя цикл событий. Кроме того, API asyncio для
подпроцессов позволяют запустить
процесс и взаимодействовать с ним из цикла событий. Наконец, упомянутый выше
метод loop.run_in_executor() также можно использовать
с concurrent.futures.ProcessPoolExecutor для выполнения
кода в другом процессе.
Выполнение блокирующего кода¶Running Blocking Code
Блокирующий (связанный с процессором) код не следует вызывать напрямую. Например, если функция выполняет интенсивные вычисления в течение 1 секунды, все параллельные задачи asyncio и операции ввода-вывода будут задержаны на 1 секунду.
Исполнитель можно использовать для выполнения задачи в другом потоке,
в том числе в другом интерпретаторе, или даже в
другом процессе, чтобы не блокировать поток ОС с
циклом событий. Подробнее см. метод loop.run_in_executor().
Журналирование¶Logging
asyncio использует модуль logging, а все логирование выполняется
через логгер "asyncio".
Уровень логирования по умолчанию – logging.INFO. Его можно легко
изменить:
logging.getLogger("asyncio").setLevel(logging.WARNING)
Сетевое логирование может блокировать цикл событий. Рекомендуется использовать отдельный поток для обработки логов или неблокирующий ввод-вывод. Пример см. в разделе «Обработчики, которые блокируют выполнение».
Обнаружение никогда не ожидаемых корутин¶Detect never-awaited coroutines
Когда корутинная функция вызывается, но не ожидается
(например, coro() вместо await coro())
или корутина не запланирована с помощью asyncio.create_task(), asyncio
выдаёт сообщение RuntimeWarning:
import asyncio
async def test():
print("never scheduled")
async def main():
test()
asyncio.run(main())
Вывод:
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
test()
Вывод в режиме отладки:
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
Coroutine created at (most recent call last)
File "../t.py", line 9, in <module>
asyncio.run(main(), debug=True)
< .. >
File "../t.py", line 7, in main
test()
test()
Обычно это исправляется либо ожиданием корутины, либо вызовом
функции asyncio.create_task():
async def main():
await test()
Обнаружение никогда не извлечённых исключений¶Detect never-retrieved exceptions
Если вызван метод Future.set_exception(), но сам объект Future никогда
не ожидается, исключение не будет передано пользовательскому коду. В этом
случае asyncio выдаёт сообщение в журнал при сборке мусора для этого Future.
Пример необработанного исключения:
import asyncio
async def bug():
raise Exception("not consumed")
async def main():
asyncio.create_task(bug())
asyncio.run(main())
Вывод:
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3>
exception=Exception('not consumed')>
Traceback (most recent call last):
File "test.py", line 4, in bug
raise Exception("not consumed")
Exception: not consumed
Включите режим отладки, чтобы получить трассировку стека на момент создания задачи:
asyncio.run(main(), debug=True)
Вывод в режиме отладки:
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3>
exception=Exception('not consumed') created at asyncio/tasks.py:321>
source_traceback: Object created at (most recent call last):
File "../t.py", line 9, in <module>
asyncio.run(main(), debug=True)
< .. >
Traceback (most recent call last):
File "../t.py", line 4, in bug
raise Exception("not consumed")
Exception: not consumed
Рекомендации по работе с асинхронными генераторами¶Asynchronous generators best practices
Чтобы писать правильный и эффективный код на asyncio, нужно знать о некоторых подводных камнях. В этом разделе собраны основные рекомендации, которые могут сэкономить часы отладки.
Явное закрытие асинхронных генераторов¶Close asynchronous generators explicitly
Рекомендуется вручную закрывать
асинхронный генератор. Если генератор
завершается досрочно – например, из-за исключения, возникшего в теле цикла
async for, – его асинхронный код очистки может выполниться в неожиданном
контексте. Это может произойти уже после завершения задач, от которых он
зависит, или при остановке цикла событий, когда срабатывает сборщик мусора
для async-генератора.
Чтобы этого избежать, явно закрывайте генератор, вызывая его метод
aclose(), или используйте контекстный менеджер
contextlib.aclosing():
import asyncio
import contextlib
async def gen():
yield 1
yield 2
async def func():
async with contextlib.aclosing(gen()) as g:
async for x in g:
break # Не выполнять итерацию до конца
asyncio.run(func())
Как уже говорилось, код очистки таких асинхронных генераторов откладывается. Следующий пример показывает, что финализация асинхронного генератора может произойти в неожиданном порядке:
import asyncio
work_done = False
async def cursor():
try:
yield 1
finally:
assert work_done
async def rows():
global work_done
try:
yield 2
finally:
await asyncio.sleep(0.1) # имитировать некоторую асинхронную работу
work_done = True
async def main():
async for c in cursor():
async for r in rows():
break
break
asyncio.run(main())
Для этого примера получаем следующий вывод:
unhandled exception during asyncio.run() shutdown
task: <Task finished name='Task-3' coro=<<async_generator_athrow without __name__>()> exception=AssertionError()>
Traceback (most recent call last):
File "example.py", line 6, in cursor
yield 1
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "example.py", line 8, in cursor
assert work_done
^^^^^^^^^
AssertionError
Асинхронный генератор cursor() был финализирован до генератора
rows – неожиданное поведение.
Пример можно исправить, явно закрыв
async-генераторы cursor и rows:
async def main():
async with contextlib.aclosing(cursor()) as cursor_gen:
async for c in cursor_gen:
async with contextlib.aclosing(rows()) as rows_gen:
async for r in rows_gen:
break
break
Создавайте асинхронные генераторы только при работающем цикле событий¶Create asynchronous generators only when the event loop is running
Рекомендуется создавать асинхронные генераторы только после того, как цикл событий уже создан.
Чтобы гарантировать надёжное закрытие асинхронных генераторов, цикл событий
использует функцию sys.set_asyncgen_hooks() для регистрации колбэков.
Эти колбэки обновляют список запущенных асинхронных генераторов, поддерживая
его в согласованном состоянии.
При вызове функции loop.shutdown_asyncgens()
запущенные генераторы корректно останавливаются, а список очищается.
Асинхронный генератор вызывает соответствующий системный хук при первой итерации. Одновременно генератор запоминает, что хук уже был вызван, и больше не вызывает его.
Поэтому, если итерация начинается до создания цикла событий, цикл событий не сможет добавить генератор в свой список активных генераторов – хуки устанавливаются уже после того, как генератор попытался их вызвать. В результате цикл событий не сможет при необходимости завершить такой генератор.
Рассмотрим следующий пример:
import asyncio
async def agenfn():
try:
yield 10
finally:
await asyncio.sleep(0)
with asyncio.Runner() as runner:
agen = agenfn()
print(runner.run(anext(agen)))
del agen
Вывод:
10
Exception ignored while closing generator <async_generator object agenfn at 0x000002F71CD10D70>:
Traceback (most recent call last):
File "example.py", line 13, in <module>
del agen
^^^^
RuntimeError: async generator ignored GeneratorExit
Этот пример можно исправить так:
import asyncio
async def agenfn():
try:
yield 10
finally:
await asyncio.sleep(0)
async def main():
agen = agenfn()
print(await anext(agen))
del agen
asyncio.run(main())
Избегайте одновременных итерации и закрытия одного и того же генератора¶Avoid concurrent iteration and closure of the same generator
Асинхронные генераторы могут быть повторно вызваны во время выполнения
другого вызова __anext__() / athrow() / aclose().
Это может привести к несогласованному состоянию async-генератора и вызвать
ошибки.
Рассмотрим следующий пример:
import asyncio
async def consumer():
for idx in range(100):
await asyncio.sleep(0)
message = yield idx
print('received', message)
async def amain():
agenerator = consumer()
await agenerator.asend(None)
fa = asyncio.create_task(agenerator.asend('A'))
fb = asyncio.create_task(agenerator.asend('B'))
await fa
await fb
asyncio.run(amain())
Вывод:
received A
Traceback (most recent call last):
File "test.py", line 38, in <module>
asyncio.run(amain())
~~~~~~~~~~~^^^^^^^^^
File "Lib/asyncio/runners.py", line 204, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "Lib/asyncio/runners.py", line 127, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "Lib/asyncio/base_events.py", line 719, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "test.py", line 36, in amain
await fb
RuntimeError: anext(): asynchronous generator is already running
Поэтому рекомендуется избегать использования асинхронных генераторов в параллельных задачах или в разных циклах событий.