8.2. Корутини та задачі¶
Корутини є одиницею роботи, з якої будується програма asyncio; задачі — це спосіб, яким програма виконує кілька корутин одночасно.
8.2.1. Корутини¶
A coroutine is a function declared with async def
import asyncio
async def heartbeat(interval_ms):
while True:
print("tick")
await asyncio.sleep_ms(interval_ms)
Тіло виглядає як звичайна функція, але з одним додатковим елементом: await. Скрізь, де корутина має чогось чекати – сну, зчитування з мережі, встановлення події – вона awaits вираз, який знає, як призупинити корутину до моменту, коли очікуване стане готовим. На кожному await корутина передає керування назад до asyncio; asyncio відновлює її з тієї самої точки, щойно очікувана операція завершиться.
Модуль asyncio містить два варіанти сну:
asyncio.sleep()– аргумент у секундах, приймає float.asyncio.sleep_ms()– аргумент у мілісекундах, приймає int. Розширення MicroPython; зазвичай правильний вибір для камери, оскільки часові параметри мікропрограми вимірюються в мілісекундах.
Просто async def сам по собі нічого не робить. Виклик heartbeat(500) не виконує тіло; він повертає об’єкт корутини, який asyncio має запланувати. Найпростіший спосіб запланувати одну корутину – це asyncio.run()
asyncio.run(heartbeat(500))
asyncio.run() запускає цикл подій, планує передану їй корутину як точку входу верхнього рівня, керує циклом до повернення цієї корутини, а потім зупиняє цикл. Для однієї корутини це і є вся програма. Для кількох корутин програма використовує задачі.
8.2.2. Задачі¶
A task is asyncio’s wrapper around a coroutine that says schedule this concurrently with the current one and let me keep going. asyncio.create_task() makes one and returns a Task object representing the scheduled work:
task = asyncio.create_task(heartbeat("fast", 100))
Корутина тепер є в розкладі циклу; виклик не чекав на неї. Повернений Task – це дескриптор, який виклик надалі використовує для взаємодії з запущеною роботою.
Отримавши дескриптор, програма може робити з ним три речі:
Чекати завершення задачі.
Taskсам по собі є awaitable.result = await taskпризупиняє поточну корутину до повернення корутиниtask, а потім відновлює роботу з тим, що та корутина повернула (або повторно генерує виняток).Скасувати задачу.
task.cancel()планує генераціюasyncio.CancelledErrorвсередині корутини задачі на її наступномуawait, надаючи їй можливість виконати код очищення в блоціfinally. Сторінка про тайм-аути та скасування охоплює деталі.Ідентифікувати її пізніше.
asyncio.current_task()повертаєTaskдля поточної запущеної корутини. Більшість скриптів ніколи не викликають його; він з’являється в інструментуванні та обробниках винятків.
Скрипту не обов’язково зберігати дескриптор щоразу. Тимчасові фонові задачі, які програма запускає та залишає працювати, можуть відкидати повернене значення – цикл все одно їх планує:
import asyncio
async def heartbeat(name, interval_ms):
while True:
print(name)
await asyncio.sleep_ms(interval_ms)
async def main():
asyncio.create_task(heartbeat("fast", 100))
asyncio.create_task(heartbeat("slow", 500))
await asyncio.sleep(5)
asyncio.run(main())
Два виклики create_task планують обидва серцебиття, не чекаючи жодного з них. Керування негайно повертається до main, яка потім awaits п’ятисекундний сон. Поки вона спить, дві задачі серцебиття виконуються; цикл перебирає задачі, готові до запуску. Через п’ять секунд main повертається, цикл зупиняє всі ще активні задачі, і asyncio.run() повертає керування виклику.
Зберігайте дескриптор щоразу, коли програма дійсно потребує однієї з трьох операцій вище. На практиці це означає майже завжди, оскільки коректне завершення програми передбачає скасування фонових задач, які вона породила – сторінка скасування охоплює цей шаблон.
8.2.3. Правило двох рядків¶
Мінімальна програма asyncio – це два рядки, якими завершуються наведені вище приклади:
async def main():
...
asyncio.run(main())
Все інше – задачі, які створює програма, примітиви, якими вона їх координує, потоки, які вона відкриває – відбувається всередині main (і всередині корутин, які main породжує). Коли скрипт переростає класичний цикл камери while True: csi0.snapshot(), відповідь – не викликати asyncio.run() в кількох місцях; відповідь – включити нову роботу до main як додаткові задачі.