Документация Python неофициальный перевод
Содержание страницы

Разработка с asyncioDeveloping with asyncio

Асинхронное программирование отличается от классического «последовательного» программирования.

На этой странице перечислены типичные ошибки и ловушки, а также объясняется, как их избежать.

Режим отладкиDebug Mode

По умолчанию asyncio работает в производственном режиме. Для упрощения разработки в asyncio есть режим отладки.

Включить режим отладки asyncio можно несколькими способами:

  • Установка переменной окружения PYTHONASYNCIODEBUG в значение 1.

  • Использование опции командной строки Python -X dev

  • Передача debug=True в asyncio.run().

  • Вызов loop.set_debug().

В дополнение к включению режима отладки также рекомендуется:

  • Установка уровня журналирования логгера asyncio на logging.DEBUG; например, следующий фрагмент кода можно выполнить при запуске приложения:

    logging.basicConfig(level=logging.DEBUG)
    
  • Настройка модуля warnings на отображение предупреждений ResourceWarning. Один из способов сделать это – использовать параметр командной строки -W default.

Когда режим отладки включён:

  • asyncio проверяет корутины, которые не были ожиданы и регистрирует их; это помогает избежать ошибки «забытого await».

  • Многие небезопасные по отношению к потокам 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 для выполнения блокирующего кода в другом потоке ОС, не блокируя поток ОС, в котором работает цикл событий.

В настоящее время нет способа напрямую планировать корутины или колбэки из другого процесса (например, запущенного с помощью 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