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