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