Spec-Zone.ru › Python 3.11

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

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

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

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

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

Существует несколько способов включения режима отладки asyncio:

  • Установка переменной окружения PYTHONASYNCIODEBUG в значение 1.
  • Использование режима разработки Python.
  • Передача debug=True в asyncio.run().
  • Вызов loop.set_debug().

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

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

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

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

  • asyncio проверяет наличие корутин, которые не были ожиданиями, и регистрирует их; это помогает избежать ловушки «забытое ожидание».
  • Многие не потокобезопасные API asyncio (такие как методы loop.call_soon() и loop.call_at()) генерируют исключение, если они вызываются из неправильной нити.
  • Время выполнения селектора ввода-вывода регистрируется, если для выполнения операции ввода-вывода требуется слишком много времени.
  • Обработчики, которые занимают более 100 миллисекунд, регистрируются. Атрибут loop.slow_callback_duration можно использовать для установки минимального времени выполнения в секундах, которое считается «медленным».

Конкурентность и многопоточность

Цикл событий выполняется в потоке (обычно в главном потоке) и выполняет все обработчики и задачи в своём потоке. Пока задача выполняется в цикле событий, другие задачи не могут выполняться в том же потоке. Когда задача выполняет выражение 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)

# 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, которые могут читать из каналов и следить за дескрипторами файлов без блокировки цикла событий. Кроме того, API подпроцессов asyncio предоставляют способ запуска процесса и взаимодействия с ним из цикла событий. Наконец, упомянутый выше метод loop.run_in_executor() также можно использовать с concurrent.futures.ProcessPoolExecutor для выполнения кода в другом процессе.

Выполнение блокирующего кода

Блокирующий (ёмкий по ЦП) код не должен вызываться напрямую. Например, если функция выполняет ресурсоёмкое вычисление в течение 1 секунды, все concurrent asyncio задачи и операции ввода-вывода будут задерживаться на 1 секунду.

Для выполнения задачи в другом потоке или даже в другом процессе, чтобы избежать блокировки операционной нити с циклом событий, можно использовать исполнителя. Подробнее см. метод loop.run_in_executor().

Ведение журнала

asyncio использует модуль logging, и все логирование выполняется через "asyncio" журнал.

Уровень журнала по умолчанию — logging.INFO, который легко настраивается:

logging.getLogger("asyncio").setLevel(logging.WARNING)

Журналирование сетевых данных может заблокировать цикл событий. Рекомендуется использовать отдельный поток для обработки журналов или использовать неблокирующий ввод-вывод. Например, см. Обработка блокирующих обработчиков.

Обнаружение корутин, которые никогда не ожидают

Когда вызывается функция корутины, но не ожидается (например, 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()

Обнаружение никогда не полученных исключений

Если вызван метод 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

© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/library/asyncio-dev.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API