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