Разработка с asyncio
Асинхронное программирование отличается от классического «последовательного» программирования.
На этой странице перечислены распространённые ошибки и ловушки, а также способы их избежания.
Режим отладки
По умолчанию asyncio работает в режиме производства. Для облегчения разработки asyncio имеет режим отладки.
Существует несколько способов включить режим отладки asyncio:
- Установите переменную окружения
PYTHONASYNCIODEBUGна значение1. - Используйте режим Разработки Python.
- Передайте значение
debug=Trueвasyncio.run(). - Вызовите
loop.set_debug().
Помимо включения режима отладки, также рекомендуется:
-
установить уровень логирования журналирования asyncio на значение
logging.DEBUG, например, следующий фрагмент кода можно выполнить при запуске приложения:logging.basicConfig(level=logging.DEBUG)
- настроить модуль
warningsдля отображения предупрежденийResourceWarning. Один из способов сделать это — использование командной строки-Wdefault.
При включенном режиме отладки:
- asyncio проверяет наличие корутин, которые не были ожидания и регистрирует их; это смягчает проблему «забытого await».
- Многие небезопасные для потоков 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 секунды, все конкурирующие задачи 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.10/library/asyncio-dev.html