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.4. Изменение общего состояния между await#

Кооперативное планирование гарантирует, что корутина владеет процессором единолично между await, но как только она awaits, может выполниться другая корутина. Если две корутины изменяют одну и ту же структуру данных шагами, включающими await, их операции могут чередоваться так, что это повредит структуру:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

Для состояния, которое изменяется только между await внутри одной корутины, синхронизация не нужна. Для состояния, изменяемого через await и доступного из более чем одной корутины, оберните критическую секцию в Lock.

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