Разработка с помощью asyncio¶
Асинхронное программирование отличается от классического «последовательного» программирования.
На этой странице перечислены распространенные ошибки и ловушки, а также объясняется, как их избежать.
Режим отладки¶
По умолчанию asyncio работает в производственном режиме. Для облегчения разработки у asyncio есть режим debug mode.
Существует несколько способов включить режим отладки asyncio:
Установка переменной окружения
PYTHONASYNCIODEBUG
в значение1
.Используя Python Development Mode.
Передача
debug=True
вasyncio.run()
.Вызывает
loop.set_debug()
.
Помимо включения режима отладки, обратите внимание на следующие моменты:
установив уровень журнала asyncio logger на
logging.DEBUG
, например, при запуске приложения можно запустить следующий фрагмент кода:logging.basicConfig(level=logging.DEBUG)
настроить модуль
warnings
на отображение предупрежденийResourceWarning
. Один из способов сделать это - использовать параметр-W
default
опции командной строки.
Когда включен режим отладки:
asyncio проверяет наличие coroutines that were not awaited и записывает их в журнал; это уменьшает опасность «забытого ожидания».
Многие непотокобезопасные Asyncio API (например, методы
loop.call_soon()
иloop.call_at()
) вызывают исключение, если они вызываются из неправильного потока.Время выполнения селектора ввода/вывода регистрируется, если на выполнение операции ввода/вывода уходит слишком много времени.
Обратные вызовы, выполняющиеся дольше 100 миллисекунд, записываются в журнал. Атрибут
loop.slow_callback_duration
можно использовать для установки минимальной продолжительности выполнения в секундах, которая считается «медленной».
Параллелизм и многопоточность¶
Цикл событий запускается в потоке (обычно в главном потоке) и выполняет все обратные вызовы и задачи в своем потоке. Пока задача выполняется в цикле событий, никакие другие задачи не могут выполняться в том же потоке. Когда задача выполняет выражение await
, выполнение задачи приостанавливается, и цикл событий выполняет следующую задачу.
Чтобы запланировать callback из другого потока ОС, следует использовать метод loop.call_soon_threadsafe()
. Пример:
loop.call_soon_threadsafe(callback, *args)
Почти все объекты asyncio не являются потокобезопасными, что обычно не является проблемой, если только не существует кода, который работает с ними вне Task или callback. Если такому коду необходимо вызвать низкоуровневый asyncio API, следует использовать метод loop.call_soon_threadsafe()
, например:
loop.call_soon_threadsafe(fut.cancel)
Чтобы запланировать выполнение объекта coroutine из другого потока ОС, следует использовать функцию run_coroutine_threadsafe()
. Она возвращает concurrent.futures.Future
для доступа к результату:
async def coro_func():
return await asyncio.sleep(1, 42)
# Later in another OS thread:
future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
# Wait for the result:
result = future.result()
Для обработки сигналов цикл событий должен выполняться в главном потоке.
Метод loop.run_in_executor()
можно использовать вместе с concurrent.futures.ThreadPoolExecutor
для выполнения блокирующего кода в другом потоке ОС без блокирования потока ОС, в котором выполняется цикл событий.
В настоящее время не существует способа запланировать корутины или обратные вызовы непосредственно из другого процесса (например, запущенного с помощью multiprocessing
). В разделе Методы циклов событий перечислены API, позволяющие читать из труб и просматривать дескрипторы файлов без блокирования цикла событий. Кроме того, Subprocess Asyncio предоставляет возможность запускать Subprocess API предоставляют возможность запускать процесс и взаимодействовать с ним из цикла событий. Наконец, вышеупомянутый метод loop.run_in_executor()
также может быть использован с concurrent.futures.ProcessPoolExecutor
для выполнения кода в другом процессе.
Выполнение блокирующего кода¶
Блокирующий (связанный с процессором) код не должен вызываться напрямую. Например, если функция выполняет требовательные к процессору вычисления в течение 1 секунды, все параллельные asyncio-задачи и операции ввода-вывода будут задержаны на 1 секунду.
Исполнитель может использоваться для запуска задачи в другом потоке или даже в другом процессе, чтобы не блокировать поток ОС циклом событий. Более подробную информацию см. в методе loop.run_in_executor()
.
Ведение журнала¶
asyncio использует модуль logging
, а все логирование осуществляется через логгер "asyncio"
.
По умолчанию используется уровень журнала logging.INFO
, который можно легко настроить:
logging.getLogger("asyncio").setLevel(logging.WARNING)
Ведение сетевых журналов может заблокировать цикл событий. Рекомендуется использовать отдельный поток для работы с логами или использовать неблокирующий ввод-вывод. Например, смотрите Работа с обработчиками, которые блокируют.
Обнаружение долгожданных корутинов¶
Если функция coroutine вызвана, но не ожидается (например, coro()
вместо await coro()
) или coroutine не запланирован с 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()
Обнаружение никогда не извлекаемых исключений¶
Если вызывается 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
Enable the debug mode, чтобы получить трассировку, где была создана задача:
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