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