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.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 ThreadSafeFlag – strona 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 przezawait asyncio.sleeplubawait 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
awaitpomię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.