8.15. Pułapki#

Te same wzorce, które czynią asyncio przyjemnym – brak wywłaszczania, jawne await – nadają mu też własny zestaw kształtów, które potrafią ugryźć. Ta strona jest katalogiem tych, które pojawiają się na tyle często, by warto było je znać.

8.15.1. Zapomnienie o await#

Wywołanie funkcji async def zwraca obiekt korutyny. Nie uruchamia ono ciała funkcji. Aby faktycznie ją wykonać, na korutynie trzeba wykonać await lub opakować ją w zadanie:

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

Błąd jest cichy – obiekt korutyny zostaje utworzony, porzucony i nigdy niewykonany. Aplikacja działa dalej, jakby wszystko zadziałało. MicroPython czasem rejestruje ostrzeżenie, że na korutynie nigdy nie wykonano await; czasem nie. Sprawdzaj brakujące await w każdym miejscu wywołania, które wygląda jak wywołanie funkcji.

8.15.2. Ciasne pętle bez await#

Korutyna działająca w pętli i nigdy niewykonująca await zawłaszcza pętlę zdarzeń. Żadne inne zadanie nie robi postępów, dopóki pętla się nie zakończy lub nie odda sterowania:

async def counter():
    n = 0
    while True:
        n += 1               # bug: starves the loop

Naprawą jest oddanie sterowania (yield) wewnątrz pętli – zazwyczaj await asyncio.sleep_ms(0) – aby inne gotowe zadania dostały szansę na działanie. Obliczeniowo wymagająca praca również należy do tego schematu: pętla przetwarzania obrazu działająca po setki milisekund na iterację powinna oddawać sterowanie co najmniej raz na iterację, aby reszta programu się nie zacinała.

8.15.3. Połykanie CancelledError#

Strona o anulowaniu omówiła to już szczegółowo. Powtarzamy to tutaj, ponieważ jest to najczęstsza przyczyna sytuacji „moja aplikacja nie chce się wyłączyć”: korutyna przechwytuje asyncio.CancelledError w celu sprzątania i zapomina go ponownie zgłosić. Zadanie działa dalej; wywołujący, który zażądał anulowania, wisi w nieskończoność, czekając na jego zakończenie. Zawsze ponownie zgłaszaj wyjątek po sprzątaniu lub użyj bloku try/finally zamiast jawnego except.

8.15.4. Mutowanie współdzielonego stanu pomiędzy await#

Harmonogramowanie kooperatywne gwarantuje, że korutyna ma CPU dla siebie pomiędzy await, ale gdy tylko wykona await, może uruchomić się inna korutyna. Jeśli dwie korutyny modyfikują tę samą strukturę danych w krokach zawierających await, ich operacje mogą się przeplatać w sposób uszkadzający strukturę:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

Dla stanu mutowanego wyłącznie pomiędzy await wewnątrz jednej korutyny żadna synchronizacja nie jest potrzebna. Dla stanu mutowanego pomiędzy await i dostępnego z więcej niż jednej korutyny opakuj sekcję krytyczną w Lock.

8.15.5. await na poziomie modułu#

await jest poprawne tylko wewnątrz ciała async def. Zapisanie go na poziomie modułu – poza jakąkolwiek korutyną – jest błędem składni:

# bug: not inside an async def
result = await fetch()

Naprawą jest umieszczenie pracy w korutynie i wywołanie jej z punktu wejścia programu asyncio.run().

8.15.6. Wielokrotne wywołania asyncio.run#

MicroPython ma jedną pętlę zdarzeń. Dwukrotne wywołanie asyncio.run() z rzędu – raz na konfigurację, raz na główną pracę – nadal korzysta z tej samej pętli. Wywołanie go z wnętrza działającej korutyny jest błędem: pętla już działa. Oba przypadki pojawiają się najczęściej, gdy skrypt rozrasta się organicznie, a autor próbuje go rozszerzyć, dodając kolejne wywołania run() zamiast włączyć nową pracę do istniejącego main.

8.15.7. Użycie Event z przerwania#

asyncio.Event.set() można bezpiecznie wywołać tylko z wnętrza pętli zdarzeń. Wywołanie go z funkcji obsługi przerwania GPIO grozi uszkodzeniem danych. Do wybudzania zadania z przerwania użyj zamiast tego ThreadSafeFlagstrona o nim omawia ten schemat.

8.15.8. Długie wywołania synchroniczne#

Korutyna może wykonać await na własnych prymitywach oczekiwania asyncio; wszystko inne, co wywoła, działa synchronicznie i blokuje pętlę do momentu powrotu. Blokujący time.sleep() na 200 ms, zapis na kartę SD trwający 80 ms do opróżnienia, duża kompresja JPEG, wywołanie csi.CSI.snapshot() – każde z nich trzyma pętlę zdarzeń przez cały swój czas trwania. Naprawa zależy od wywołania:

  • Dla time.sleep: zastąp je przez await asyncio.sleep lub await asyncio.sleep_ms.

  • Dla csi.CSI.snapshot: użyj asynchronicznego wrappera zrzutu obrazu, który buduje strona o przechwytywaniu.

  • Dla długich obliczeń (przetwarzanie obrazu, kodowanie JPEG): zaakceptuj koszt lub podziel pracę na fragmenty wykonujące await pomiędzy iteracjami.

Asyncio nie może uczynić wywołania synchronicznego nieblokującym. Może jedynie pozwolić innym korutynom działać podczas, gdy coś innego oczekuje.