8.2. Korutyny i zadania

Korutyny to jednostka pracy, z której zbudowany jest program asyncio; zadania to sposób, w jaki aplikacja uruchamia kilka korutyn współbieżnie.

8.2.1. Korutyny

Korutyna to funkcja zadeklarowana za pomocą async def

import asyncio

async def heartbeat(interval_ms):
    while True:
        print("tick")
        await asyncio.sleep_ms(interval_ms)

Ciało wygląda jak zwykła funkcja, z jednym dodatkowym składnikiem: await. Wszędzie tam, gdzie korutyna musi na coś poczekać — uśpienie, odczyt z sieci, ustawienie zdarzenia — wykonuje awaitwyrażenia, które potrafi zawiesić korutynę do momentu, aż to, na co czeka, będzie gotowe. Przy każdym await korutyna oddaje sterowanie do asyncio; asyncio wznawia ją od tego samego punktu, gdy oczekiwana operacja się zakończy.

Moduł asyncio dostarcza dwie funkcje uśpienia:

  • asyncio.sleep() — argument w sekundach, przyjmuje liczbę zmiennoprzecinkową.

  • asyncio.sleep_ms() — argument w milisekundach, przyjmuje liczbę całkowitą. Rozszerzenie MicroPython; zwykle właściwy wybór na kamerze, ponieważ pokrętła czasowe w oprogramowaniu układowym mają postać milisekundową.

Samo async def nie robi nic samo z siebie. Wywołanie heartbeat(500) nie wykonuje ciała; zwraca obiekt korutyny, który asyncio musi zaszeregować. Najprostszym sposobem zaszeregowania jednej jest asyncio.run()

asyncio.run(heartbeat(500))

asyncio.run() uruchamia pętlę zdarzeń, szereguje przekazaną korutynę jako punkt wejścia najwyższego poziomu, steruje pętlą do momentu, aż ta korutyna zwróci wynik, a następnie likwiduje pętlę. Dla pojedynczej korutyny to cały program. Dla kilku korutyn aplikacja sięga po zadania.

8.2.2. Zadania

Zadanie to opakowanie asyncio wokół korutyny, które mówi zaszereguj ją współbieżnie z bieżącą i pozwól mi działać dalej. asyncio.create_task() tworzy je i zwraca obiekt Task reprezentujący zaszeregowaną pracę:

task = asyncio.create_task(heartbeat("fast", 100))

Korutyna jest teraz w harmonogramie pętli; wywołujący na nią nie czekał. Zwrócony Task to uchwyt, którego wywołujący używa później do interakcji z tą działającą pracą.

Gdy aplikacja ma już uchwyt, może z nim zrobić trzy rzeczy:

  • Poczekać na zakończenie zadania. Task sam jest awaitable. result = await task zawiesza bieżącą korutynę do momentu, aż korutyna task zwróci wynik, a następnie wznawia z tym, co ta korutyna zwróciła (lub ponownie zgłasza to, co zgłosiła).

  • Anulować zadanie. task.cancel() szereguje zgłoszenie asyncio.CancelledError wewnątrz korutyny zadania przy jej następnym await, dając jej szansę na wykonanie kodu sprzątającego w bloku finally. Strona o limitach czasu i anulowaniu omawia szczegóły.

  • Zidentyfikować je później. asyncio.current_task() zwraca Task dla korutyny, która jest aktualnie wykonywana. Większość skryptów nigdy jej nie wywołuje; pojawia się w instrumentacji i w obsłudze wyjątków.

Skrypt nie musi przechwytywać uchwytu za każdym razem. Jednorazowe zadania w tle, które aplikacja uruchamia i pozostawia działające, mogą pominąć zwracaną wartość — pętla i tak je szereguje:

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())

Dwa wywołania create_task szeregują oba sygnały heartbeat, nie czekając na żaden z nich. Sterowanie natychmiast wraca do main, który następnie wykonuje awaitna pięciosekundowym uśpieniu. Podczas gdy śpi, dwa zadania heartbeat posuwają się naprzód; pętla przechodzi przez to zadanie, które jest gotowe do działania. Po pięciu sekundach main zwraca wynik, pętla likwiduje wszystkie zadania, które wciąż żyją, a asyncio.run() wraca do wywołującego.

Przechwytuj uchwyt zawsze, gdy aplikacja faktycznie potrzebuje jednej z trzech powyższych operacji. W praktyce oznacza to niemal zawsze, ponieważ czyste zamknięcie aplikacji oznacza anulowanie uruchomionych przez nią zadań w tle — strona o anulowaniu omawia ten wzorzec.

8.2.3. Reguła dwóch linii

Minimalny program asyncio to te dwie linie, którymi kończą się powyższe przykłady:

async def main():
    ...

asyncio.run(main())

Wszystko inne — zadania, które aplikacja tworzy, prymitywy, którymi je koordynuje, strumienie, które otwiera — dzieje się wewnątrz main (oraz wewnątrz korutyn, które main uruchamia). Gdy skrypt przerasta klasyczną pętlę kamery while True: csi0.snapshot(), odpowiedzią nie jest wywoływanie asyncio.run() w kilku miejscach; jest nią wplecenie nowej pracy do main jako kolejnych zadań.