Разработка на asyncio¶
Асинхронное программирование отличается от классического последовательного программирования.
На этой странице перечислены распространенные ошибки, ловушки и описаности, а также способы их избежания.
Режим отладки¶
По умолчанию asyncio выполняется в продакшен режиме. Для облегчения асинхронной разработки содержит режим отладки.
Существует несколько способов активации режима асинхронной отладки:
- Установка для переменной среды
PYTHONASYNCIODEBUG
в значение1
- Использование параметра командной строки
-X
dev
Python - Передача
debug=True
asyncio.run()
- Вызов
loop.set_debug()
.
В дополнение к включению режима отладки, рассмотрим также:
Установка уровня журналирования asyncio logger в
logging.DEBUG
, например, следующий сниппет кода который может быть запущен при запуске приложения:logging.basicConfig(level=logging.DEBUG)
Конфигурирование модуля
warnings
для отображенияResourceWarning
предупреждений. Одним из способов этого является использование параметра командной строки-W
default
.
Если включен режим отладки:
- Asyncio проверяет наличие корутин, которые не ожидают и журналирует их; это смягчает «забытое ожидание».
- Многие не потокобезопасные asyncio интерфейсы API (например,
loop.call_soon()
иloop.call_at()
методы) создают исключение, если они вызываются из неправильного потока. - Журналирование времени выполнения селектора I/O, если для выполнения операции I/O требуется слишком много времени.
- Журналирование коллбэков, занимающие более 100 мс. Атрибут
loop.slow_callback_duration
можно использовать для установки минимальной продолжительности выполнения в секундах, которая считается «медленной».
Параллелизм и многопоточность¶
Цикл событий выполняется в потоке (как правило, основном потоке) и все обратные
вызовы и задачи в потоке. Во время выполнения Task в цикле событий, никакие
другие задачи не могут выполняться в том же потоке. Когда Task выполняет
выражение await
, выполняемая задача приостанавливается, и цикл событий
выполняет следующую задачу.
Чтобы запланировать колбэк из другого потока ОС, следует использовать
метод loop.call_soon_threadsafe()
. Пример:
loop.call_soon_threadsafe(callback, *args)
Почти все asyncio объекты не являются потокобезопасными, что обычно не
является проблемой, если нет кода, который работает с ними извне Task
или обратного вызова. Если необходимо, чтобы такий код вызывал
низкоуровневый asyncio API, то следует использовать метод 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
, чтобы выполнить
блокировка код в другом потоке оС без блокировки потока оС , в котором
выполняется цикл событий.
В настоящее время нет возможности планировать корутины или обратные вызовы
непосредственно из другого процесса (например, запущенного с
multiprocessing
). В разделе Методы цикла событий
перечислены API-интерфейсы, которые могут читать из каналов и
просматривать дескрипторы файлов, не блокируя цикл событий. Кроме того, API-
интерфейсы asyncio Подпроцессы предоставляют способ
запуска процесса и взаимодействия с ним из цикла событий. Наконец,
вышеупомянутый метод loop.run_in_executor()
также может использоваться с
concurrent.futures.ProcessPoolExecutor
для выполнения кода в другом
процессе.
Выполнение блокирующего кода¶
Блокирующий (связанный с CPU) код не должны вызываться напрямую. Например, если функция выполняет вычисления с интенсивным использованием CPU в течение 1 секунды, все конкурентные asyncio Task и операции ввода-вывода будут отложены на 1 секунду.
Исполнитель может быть использован для запуска задачи в другом потоке или даже в
другом процессе, чтобы избежать блокировки потока ОС с
циклом событий. Для получения больших подробностей см. документацию по метод
loop.run_in_executor()
.
Логгирование¶
Asyncio использует модуль logging
и выполняет журналирование через
логгер "asyncio"
.
По умолчанию уровень журналирования logging.INFO
, который может быть легко
изменён:
logging.getLogger("asyncio").setLevel(logging.WARNING)
Обнаружение never-awaited корутин¶
Когда вызывается функция корутина, без await (например, 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()
Фикситс это добавлением await корутине, либо вызвать функцию asyncio.create_task()
:
async def main():
await test()
Обнаружение неперехваченных исключений¶
Если вызывается Future.set_exception()
и объект Future не содержит await, исключение
никогда не будет распространяться выше по пользовательсому коду. В этом случае
asyncio сделает сообщение в журнале, когда объект Future будет удаляться
сборщиком мусора.
Пример необработанного исключения:
import asyncio
async def bug():
raise Exception("not consumed")
async def main():
asyncio.create_task(bug())
asyncio.run(main())
Вывод:
Исключение задачи никогда не будет получено
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