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 іноді реєструє попередження про те, що корутина так і не була await-нута; іноді – ні. Перевіряйте всі місця виклику, де має бути 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 не може зробити синхронний виклик неблокуючим. Вона може лише дозволити іншим корутинам виконуватися, поки щось інше очікує.