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 як додаткові задачі.