8.1. Współbieżność kooperacyjna

Model szeregowania zadań w asyncio jest kooperacyjny, a nie wywłaszczający. To rozróżnienie jest najważniejszym modelem mentalnym, na którym opiera się reszta tej sekcji, dlatego warto je ustalić, zanim pojawi się jakikolwiek kod.

8.1.1. Wywłaszczanie a kooperacja

Szeregowanie wywłaszczające — takie, jakiego używa system operacyjny komputera, aby utrzymać jednocześnie działanie wielu programów — może wstrzymać dowolny fragment aktualnie wykonywanego kodu w dowolnym momencie i przełączyć się na inny. Wykonywany kod nie musi robić niczego szczególnego; szereg zadań przerywa go. Czyni to szeregowanie wywłaszczające bardzo elastycznym (żaden fragment kodu nie może zagłodzić pozostałych przez swoją powolność), ale oznacza też, że każdą współdzieloną zmienną trzeba starannie chronić, ponieważ przełączenie może nastąpić w dowolnym miejscu — nawet w połowie zapisywania wartości lub w trakcie odczytu listy.

Szereg zadań kooperacyjny może przełączać się między fragmentami kodu wyłącznie w punktach, w których aktualnie wykonywany fragment jawnie oddaje sterowanie. W asyncio takimi punktami są każde await oraz każde wywołanie korutyny, która wewnętrznie oddaje sterowanie (najczęściej asyncio.sleep()). Pomiędzy dwoma instrukcjami await wykonywana korutyna ma procesor wyłącznie dla siebie.

Wynikają z tego dwie konsekwencje:

  • Korutyna, która nigdy nie wykonuje await, nigdy nie zostaje wstrzymana. Jeśli korutyna tkwi w ciasnej pętli bez await w środku, zawłaszcza szereg zadań i nic innego nie posuwa się naprzód. Rozwiązaniem jest wykonanie await asyncio.sleep_ms(0) (lub innego wywołania oczekującego) w rozsądnym punkcie pętli.

  • Stan współdzielony jest bezpieczny pomiędzy instrukcjami await. Dwie korutyny nie mogą przeplatać się w połowie operacji, która nie zawiera w sobie await. Rodzaj uszkodzenia danych powstający, gdy wywłaszczenie trafia w środek wieloetapowej aktualizacji — jeden fragment kodu odczytuje wartość, podczas gdy inny jest w trakcie jej zmiany — po prostu nie może tu wystąpić. Koordynacja między korutynami jest nadal potrzebna, gdy kilka z nich musi współdzielić zasób na przestrzeni instrukcji await, ale problem przeplatania w środku jednej linii tu nie obowiązuje.

8.1.2. Trzy warstwy

Każdy skrypt asyncio zbudowany jest z tych samych trzech warstw. Kolejne dwie strony omawiają je szczegółowo; oto etykiety, które warto mieć na uwadze podczas ich czytania.

  • Korutyny — funkcje zadeklarowane za pomocą async def, z których każda stanowi samodzielną jednostkę pracy i wykonuje await tam, gdzie to właściwe. Przegląd języka Python wprowadził słowa kluczowe async/await; w asyncio są one sposobem, w jaki korutyna oddaje sterowanie szeregowi zadań.

  • Zadania — opakowanie, które asyncio.create_task() nakłada na korutynę, aby zaszeregować ją współbieżnie z bieżącą. Aplikacja zazwyczaj tworzy kilka zadań dla długotrwałych prac (pętla zrzutów obrazu, klient sieciowy, czytnik UART, …).

  • Pętla zdarzeń — silnik leżący u podstaw, który śledzi, które korutyny czekają, a które są gotowe do działania, przełączając się między zadaniami przy każdym await. Aplikacja nie pisze pętli; przekazuje korutynę najwyższego poziomu do asyncio.run(), a pętla steruje stamtąd wszystkim.

Gdy aplikacja jest opisana w ten sposób — jako niewielki zbiór korutyn złożony przez pętlę zdarzeń — współbieżność staje się właściwością kształtu programu, a nie czymś, czym aplikacja musi zarządzać krok po kroku.