8.15. Подводные камни#
Те же паттерны, которые делают asyncio приятным – отсутствие вытеснения, явные awaits – порождают собственный набор ситуаций, которые могут навредить. Эта страница – каталог тех из них, что встречаются достаточно часто, чтобы о них стоило знать.
8.15.1. Забытый await#
Вызов функции async def возвращает объект корутины. Он не выполняет тело функции. Чтобы действительно его выполнить, корутину нужно awaitить или обернуть в задачу:
async def main():
send_request() # bug: returns the coroutine, does nothing
await send_request() # right: run it to completion
asyncio.create_task(send_request()) # right: run it concurrently
Ошибка молчаливая – объект корутины создаётся, отбрасывается и никогда не выполняется. Приложение продолжает работу так, будто всё сработало. MicroPython иногда выводит предупреждение о том, что корутина так и не была ожидаема; иногда нет. Проверяйте на пропущенные awaits каждое место вызова, которое выглядит как вызов функции.
8.15.2. Плотные циклы без await#
Корутина, которая работает в цикле и никогда не awaits, монополизирует цикл событий. Ни одна другая задача не продвигается, пока цикл не завершится или не уступит управление:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
Решение – уступка управления внутри цикла – обычно await asyncio.sleep_ms(0) – чтобы другие готовые задачи получили шанс выполниться. Вычислительно тяжёлая работа тоже относится к этой форме: цикл обработки изображений, выполняющийся сотни миллисекунд за итерацию, должен уступать управление хотя бы раз за итерацию, чтобы остальная часть программы не зависала.
8.15.3. Проглатывание CancelledError#
На странице об отмене это уже подробно рассматривалось. Повторяем здесь, потому что это самая частая причина проблемы «моё приложение не завершается»: корутина перехватывает asyncio.CancelledError для целей очистки и забывает повторно его возбудить. Задача продолжает работать; вызывающая сторона, запросившая отмену, бесконечно ждёт её завершения. Всегда повторно возбуждайте исключение после очистки или используйте блок try/finally вместо явного except.
8.15.5. await на уровне модуля#
await допустим только внутри тела async def. Запись его на уровне модуля – вне какой-либо корутины – является синтаксической ошибкой:
# bug: not inside an async def
result = await fetch()
Решение – поместить работу в корутину и вызвать её из точки входа программы asyncio.run().
8.15.6. Несколько вызовов asyncio.run#
В MicroPython один цикл событий. Вызов asyncio.run() дважды подряд – один раз для настройки, один раз для основной работы – всё равно использует тот же цикл. Вызов его изнутри работающей корутины является ошибкой: цикл уже запущен. Оба случая чаще всего возникают, когда скрипт растёт органически, и автор пытается расширить его, добавляя новые вызовы run() вместо того, чтобы влить новую работу в существующий main.
8.15.7. Использование Event из прерывания#
asyncio.Event.set() безопасно вызывать только изнутри цикла событий. Вызов его из обработчика прерывания GPIO создаёт опасность повреждения. Для пробуждения задачи из прерывания используйте вместо этого ThreadSafeFlag – страница о нём описывает эту форму.
8.15.8. Длительные синхронные вызовы#
Корутина может ожидать собственные примитивы ожидания asyncio; всё остальное, что она вызывает, выполняется синхронно и блокирует цикл до своего возврата. Блокирующий time.sleep() на 200 мс, запись на SD-карту, на сброс которой уходит 80 мс, сжатие большого JPEG, вызов csi.CSI.snapshot() – каждый из них удерживает цикл событий на всё своё время. Решение зависит от вызова:
Для
time.sleep: замените его наawait asyncio.sleepилиawait asyncio.sleep_ms.Для
csi.CSI.snapshot: используйте асинхронную обёртку snapshot, которую строит страница о захвате.Для длительных вычислений (обработка изображений, кодирование JPEG): примите эту цену или разбейте работу на части, которые
awaitмежду итерациями.
Asyncio не может сделать синхронный вызов неблокирующим. Он может лишь позволить другим корутинам выполняться, пока что-то иное ожидается.