To pytanie jest motywowane moim kolejnym pytaniem: Jak czekać w cdef?
W sieci jest mnóstwo artykułów i postów na blogach asyncio
, ale wszystkie są bardzo powierzchowne. Nie mogłem znaleźć żadnych informacji o tym, jak asyncio
faktycznie jest zaimplementowany i co sprawia, że operacje we / wy są asynchroniczne. Próbowałem odczytać kod źródłowy, ale to tysiące wierszy kodu C nie najwyższej klasy, z których wiele dotyczy obiektów pomocniczych, ale co najważniejsze, trudno jest połączyć składnię Pythona z kodem C, który by przetłumaczył w.
Dokumentacja własna Asycnio jest jeszcze mniej pomocna. Nie ma tam żadnych informacji o tym, jak to działa, tylko kilka wskazówek, jak z niego korzystać, które są czasem mylące / bardzo słabo napisane.
Jestem zaznajomiony z implementacją coroutines w Go i miałem nadzieję, że Python zrobił to samo. Gdyby tak było, kod, który wymyśliłem w powyższym poście, zadziałałby. Ponieważ tak się nie stało, próbuję teraz dowiedzieć się, dlaczego. Jak dotąd moje przypuszczenie jest następujące, proszę poprawić mnie tam, gdzie się mylę:
- Definicje procedur formularza
async def foo(): ...
są w rzeczywistości interpretowane jako metody dziedziczenia klascoroutine
. - Być może w
async def
rzeczywistości jest podzielony na wiele metod przezawait
instrukcje, w których obiekt, na którym te metody są wywoływane, jest w stanie śledzić dotychczasowy postęp, jaki poczynił podczas wykonywania. - Jeśli powyższe jest prawdą, to w zasadzie wykonanie programu sprowadza się do wywołania metod obiektu coroutine przez jakiegoś globalnego menedżera (pętlę?).
- Menedżer globalny jest w jakiś sposób (jak?) Świadomy, kiedy operacje we / wy są wykonywane przez kod Pythona (tylko?) I jest w stanie wybrać jedną z oczekujących metod standardowych do wykonania po tym, jak bieżąca metoda wykonawcza zrezygnowała z kontroli (trafienie w
await
instrukcję ).
Innymi słowy, oto moja próba „desugeringu” jakiejś asyncio
składni w coś bardziej zrozumiałego:
async def coro(name):
print('before', name)
await asyncio.sleep()
print('after', name)
asyncio.gather(coro('first'), coro('second'))
# translated from async def coro(name)
class Coro(coroutine):
def before(self, name):
print('before', name)
def after(self, name):
print('after', name)
def __init__(self, name):
self.name = name
self.parts = self.before, self.after
self.pos = 0
def __call__():
self.parts[self.pos](self.name)
self.pos += 1
def done(self):
return self.pos == len(self.parts)
# translated from asyncio.gather()
class AsyncIOManager:
def gather(*coros):
while not every(c.done() for c in coros):
coro = random.choice(coros)
coro()
Jeśli moje przypuszczenie okaże się słuszne: mam problem. Jak właściwie przebiega I / O w tym scenariuszu? W osobnym wątku? Czy cały tłumacz jest zawieszony, a wejścia / wyjścia odbywają się poza tłumaczem? Co dokładnie oznacza I / O? Jeśli moja procedura w Pythonie nazywa się procedurą C open()
, a ona z kolei wysłała przerwanie do jądra, zrzekając się kontroli nad nim, w jaki sposób interpreter Pythona wie o tym i jest w stanie kontynuować wykonywanie innego kodu, podczas gdy kod jądra wykonuje rzeczywiste operacje we / wy i dopóki budzi procedurę Pythona, która pierwotnie wysłała przerwanie? W jaki sposób interpreter języka Python może w zasadzie być tego świadomy?
źródło
BaseEventLoop
zaimplementowano CPython : github.com/python/cpython/blob/ ..._run_once
to jedyna użyteczna funkcja w całym tym module, która jest „prywatna”? Wdrożenie jest okropne, ale to mniejszy problem. Dlaczego jedyna funkcja, jaką kiedykolwiek chciałbyś wywołać w pętli zdarzeń, jest oznaczona jako „nie wzywaj mnie”?_run_once
w pierwszej kolejności?_run_once
?asyncio
jest złożona i ma swoje wady, ale prosimy o zachowanie dyskrecji. Nie oszukuj programistów stojących za kodem, którego sam nie rozumiesz.Odpowiedzi:
Jak działa asyncio?
Zanim odpowiemy na to pytanie, musimy zrozumieć kilka podstawowych terminów, pomiń je, jeśli już znasz któreś z nich.
Generatory
Generatory to obiekty, które pozwalają nam zawiesić wykonanie funkcji Pythona. Generatory wybrane przez użytkownika są implementowane przy użyciu słowa kluczowego
yield
. Tworząc normalną funkcję zawierającąyield
słowo kluczowe, zamieniamy tę funkcję w generator:Jak widać, wywołanie
next()
generatora powoduje, że interpreter ładuje ramkę testu i zwracayield
wartość ed. Wywołanienext()
ponownie powoduje ponowne załadowanie ramki do stosu interpretera i kontynuowanieyield
kolejnej wartości.Za trzecim razem
next()
nasz generator był skończony iStopIteration
został wyrzucony.Komunikacja z generatorem
Mniej znaną cechą generatorów jest to, że można się z nimi komunikować na dwa sposoby:
send()
ithrow()
.Po wywołaniu
gen.send()
wartość jest przekazywana jako wartość zwracana przezyield
słowo kluczowe.gen.throw()
z drugiej strony umożliwia rzucanie wyjątków wewnątrz generatorów, z wyjątkiem wywoływanego w tym samym miejscuyield
.Zwracanie wartości z generatorów
Zwrócenie wartości z generatora powoduje umieszczenie wartości wewnątrz
StopIteration
wyjątku. Możemy później odzyskać wartość z wyjątku i wykorzystać ją do naszych potrzeb.Oto nowe słowo kluczowe:
yield from
Python 3.4 przyszedł z dodaniem nowego hasła:
yield from
. Co to słowo pozwala nam zrobić, to przekazać każdynext()
,send()
athrow()
do generatora wewnętrznego, najbardziej zagnieżdżonych. Jeśli generator wewnętrzny zwraca wartość, jest to również wartość zwracana zyield from
:Napisałem artykuł, aby dalej rozwinąć ten temat.
Kładąc wszystko razem
Po wprowadzeniu nowego słowa kluczowego
yield from
w Pythonie 3.4 mogliśmy teraz tworzyć generatory wewnątrz generatorów, które podobnie jak tunel przekazują dane tam iz powrotem z generatorów najbardziej wewnętrznych do najbardziej zewnętrznych. To zrodziło nowe znaczenie dla generatorów - coroutines .Coroutines to funkcje, które można zatrzymać i wznowić podczas działania. W Pythonie są definiowane za pomocą
async def
słowa kluczowego. Podobnie jak generatory, oni też korzystać z własnej formyyield from
, która jestawait
. Przed wprowadzeniemasync
iawait
wprowadzeniem w Pythonie 3.5 tworzyliśmy procedury w dokładnie taki sam sposób, jak generatory (zyield from
zamiastawait
).Jak każdy iterator lub generator, który implementuje
__iter__()
metodę, programy te implementują,__await__()
co pozwala im kontynuować działanie za każdym razem, gdyawait coro
zostanie wywołany.Jest ładny sekwencja schemat wewnątrz docs Pythona , które należy sprawdzić.
W asyncio oprócz podstawowych funkcji mamy 2 ważne obiekty: zadania i przyszłość .
Futures
Futures to obiekty, które mają
__await__()
zaimplementowaną metodę, a ich zadaniem jest utrzymanie określonego stanu i wyniku. Stan może być jednym z następujących:fut.cancel()
fut.set_result()
lub przez zestaw wyjątków za pomocąfut.set_exception()
Rezultatem, tak jak się domyślasz, może być obiekt Pythona, który zostanie zwrócony, lub wyjątek, który może zostać zgłoszony.
Inną ważną cechą
future
obiektów jest to, że zawierają metodę o nazwieadd_done_callback()
. Ta metoda umożliwia wywoływanie funkcji zaraz po wykonaniu zadania - niezależnie od tego, czy zgłosiło wyjątek, czy zostało zakończone.Zadania
Obiekty zadań są specjalnymi przyszłościami, które owijają się wokół programów i komunikują się z najbardziej wewnętrznymi i najbardziej zewnętrznymi programami. Za każdym razem, gdy program jest
await
przyszłością, przyszłość jest przekazywana z powrotem do zadania (tak jak wyield from
) i zadanie ją otrzymuje.Następnie zadanie wiąże się z przyszłością. Robi to, odwołując
add_done_callback()
się do przyszłości. Odtąd, jeśli przyszłość kiedykolwiek zostanie wykonana, poprzez anulowanie, przekazanie wyjątku lub przekazanie w rezultacie obiektu Pythona, zostanie wywołane wywołanie zwrotne zadania i powróci do istnienia.Asyncio
Ostatnie palące pytanie, na które musimy odpowiedzieć, brzmi: w jaki sposób wdrażane jest IO?
W głębi asyncio mamy pętlę zdarzeń. Pętla zdarzeń zadań. Zadaniem pętli zdarzeń jest wywoływanie zadań za każdym razem, gdy są one gotowe, i koordynowanie całego wysiłku w jednej działającej maszynie.
Część IO pętli zdarzeń jest zbudowana na jednej kluczowej funkcji o nazwie
select
. Select to funkcja blokująca, zaimplementowana przez system operacyjny poniżej, która umożliwia czekanie w gniazdach na dane przychodzące lub wychodzące. Po odebraniu danych budzi się i zwraca gniazda, które odebrały dane, lub gniazda gotowe do zapisu.Podczas próby odebrania lub wysłania danych przez gniazdo za pośrednictwem asyncio, to, co dzieje się poniżej, polega na tym, że gniazdo jest najpierw sprawdzane, czy zawiera dane, które można natychmiast odczytać lub wysłać. Jeśli jego
.send()
bufor jest pełny lub.recv()
bufor jest pusty, gniazdo jest rejestrowane wselect
funkcji (po prostu dodając je do jednej z listrlist
dlarecv
iwlist
dlasend
), a odpowiednia funkcja jestawait
nowo utworzonymfuture
obiektem, powiązanym z tym gniazdem.Gdy wszystkie dostępne zadania czekają na przyszłość, pętla zdarzeń wywołuje
select
i czeka. Kiedy jedno z gniazd ma przychodzące dane lub jegosend
bufor jest wyczerpany, asyncio sprawdza przyszły obiekt powiązany z tym gniazdem i ustawia go na gotowe.Teraz dzieje się cała magia. Przyszłość jest gotowa do wykonania, zadanie, które dodało się wcześniej z
add_done_callback()
,.send()
ożywa i wywołuje proces, który wznawia najbardziej wewnętrzną procedurę (z powoduawait
łańcucha) i odczytujesz nowo otrzymane dane z pobliskiego bufora. został rozlany do.Łańcuch metod ponownie, w przypadku
recv()
:select.select
czeka.future.set_result()
jest nazywany.add_done_callback()
jest teraz obudzone..send()
przywołuje program, który przechodzi do najbardziej wewnętrznego programu i budzi go.Podsumowując, asyncio wykorzystuje możliwości generatora, które pozwalają na wstrzymywanie i wznawianie funkcji. Wykorzystuje
yield from
możliwości, które pozwalają na przekazywanie danych tam iz powrotem z generatora najbardziej wewnętrznego do najbardziej zewnętrznego. Używa ich wszystkich w celu zatrzymania wykonywania funkcji podczas oczekiwania na zakończenie operacji we / wy (za pomocąselect
funkcji systemu operacyjnego ).A co najlepsze? Podczas gdy jedna funkcja jest wstrzymana, inna może działać i przeplatać się z delikatną tkaniną, którą jest asyncio.
źródło
yield from
działanie generatorów . Zauważyłem jednak na górze, że można to pominąć na wypadek, gdyby czytelnik już o tym wiedział :-) Czy uważasz, że coś jeszcze powinienem dodać?select
Mogą kwalifikować się jako dobrze, ponieważ jest to w jaki sposób non-blocking I / O System nazywa prace nad OS. Rzeczywisteasyncio
konstrukcje i pętla zdarzeń to po prostu kod na poziomie aplikacji zbudowany z tych rzeczy.Mówienie o
async/await
iasyncio
to nie to samo. Pierwsza to fundamentalna konstrukcja niskiego poziomu (coroutines), a druga to biblioteka korzystająca z tych konstrukcji. I odwrotnie, nie ma jednej ostatecznej odpowiedzi.Poniżej znajduje się ogólny opis działania bibliotek
async/await
iasyncio
podobnych. Oznacza to, że na górze mogą być inne sztuczki (są ...), ale nie mają one znaczenia, chyba że sam je zbudujesz. Różnica powinna być znikoma, chyba że wiesz już wystarczająco dużo, aby nie musieć zadawać takiego pytania.1. Korekty a podprogramy w łupinie orzecha
Podobnie jak podprogramy (funkcje, procedury, ...), procedury (generatory, ...) są abstrakcją stosu wywołań i wskaźnika instrukcji: istnieje stos wykonujących się fragmentów kodu, a każdy znajduje się przy określonej instrukcji.
Rozróżnienie między
def
wersjamiasync def
służy jedynie przejrzystości. Rzeczywista różnica jest wreturn
porównaniuyield
. Z tegoawait
lubyield from
weź różnicę z pojedynczych wywołań do całych stacków.1.1. Podprogramy
Podprocedura reprezentuje nowy poziom stosu, na którym będą przechowywane zmienne lokalne, oraz pojedyncze przejście jego instrukcji, aby osiągnąć koniec. Rozważ podprogram taki jak ten:
To znaczy, kiedy go uruchamiasz
bar
iqux
return
, umieść jego wartość na stosie wywołańWarto zauważyć, że 4. oznacza, że podprogram zawsze zaczyna się w tym samym stanie. Wszystko, co dotyczy samej funkcji, zostaje utracone po zakończeniu. Funkcji nie można wznowić, nawet jeśli są po niej instrukcje
return
.1.2. Korekty jako trwałe podprogramy
Program jest podobny do podprogramu, ale może wyjść bez niszczenia jego stanu. Rozważmy taki program:
To znaczy, kiedy go uruchamiasz
bar
iqux
yield
, umieść jego wartość na stosie wywołań, ale zapisz stos i wskaźnik instrukcjiyield
, przywróć stos i wskaźnik instrukcji i wypchnij argumenty doqux
return
, umieść jego wartość na stosie wywołańZwróć uwagę na dodanie 2.1 i 2.2 - program można zawiesić i wznowić we wcześniej określonych punktach. Jest to podobne do zawieszenia podprogramu podczas wywoływania innego podprogramu. Różnica polega na tym, że aktywny coroutine nie jest ściśle powiązany ze swoim stosem wywołań. Zamiast tego zawieszony program jest częścią oddzielnego, izolowanego stosu.
Oznacza to, że zawieszone programy można dowolnie przechowywać lub przenosić między stosami. Każdy stos wywołań, który ma dostęp do programu, może zdecydować o jego wznowieniu.
1.3. Przechodzenie przez stos wywołań
Jak dotąd, nasz coroutine idzie w dół stosu wywołań tylko z
yield
. Podprogram może zejść w dół i w górę stosu wywołań za pomocąreturn
i()
. W celu zapewnienia kompletności, procedury potrzebują również mechanizmu, aby przejść w górę stosu wywołań. Rozważmy taki program:Po uruchomieniu oznacza to, że nadal alokuje stos i wskaźnik instrukcji jak podprogram. Kiedy się zawiesza, nadal przypomina to zapisywanie podprogramu.
Jednak
yield from
robi jedno i drugie . Zawiesza stos i wskaźnik instrukcjiwrap
i działacofoo
. Zauważ, żewrap
pozostaje zawieszony docofoo
całkowitego zakończenia. Zawsze, gdycofoo
zawiesza się lub coś jest wysyłane,cofoo
jest bezpośrednio podłączane do stosu wywołań.1.4. Korekty w dół
Jak ustalono,
yield from
umożliwia połączenie dwóch zakresów w innym pośrednim. W przypadku zastosowania rekurencyjnego oznacza to, że góra stosu może być połączona z dnem stosu.Zwróć na to uwagę
root
icoro_b
nie wiedzcie o sobie. To sprawia, że programy są znacznie czystsze niż wywołania zwrotne: programy nadal są zbudowane na relacji 1: 1, podobnie jak podprogramy. Koordynatorzy zawieszają i wznawiają cały istniejący stos wykonania aż do zwykłego punktu wywołania.W szczególności
root
może mieć dowolną liczbę programów do wznowienia. Jednak nigdy nie może wznowić więcej niż jednego w tym samym czasie. Korekty tego samego rdzenia są współbieżne, ale nie równoległe!1.5. Pythona
async
iawait
Wyjaśnieniem tej pory jednoznacznie używany
yield
iyield from
słownictwo generatorów - funkcjonalność bazowym jest taka sama. Nowa składnia Pythona 3.5async
iawait
istnieje głównie dla przejrzystości.Instrukcje
async for
iasync with
są potrzebne, ponieważ przerwałbyśyield from/await
łańcuch instrukcjami gołymifor
iwith
.2. Anatomia prostej pętli zdarzeń
Sam w sobie program nie ma koncepcji poddania kontroli innemu programowi. Może przekazać kontrolę tylko wywołującemu na dole stosu coroutine. Ten wywołujący może następnie przełączyć się na inny program i uruchomić go.
Ten węzeł główny kilku programów jest zwykle pętlą zdarzeń : w przypadku zawieszenia, program generuje zdarzenie, od którego chce wznowić. Z kolei pętla zdarzeń jest w stanie skutecznie czekać na wystąpienie tych zdarzeń. Dzięki temu może zdecydować, który program ma zostać uruchomiony jako następny lub jak czekać przed wznowieniem.
Taki projekt oznacza, że istnieje zestaw predefiniowanych zdarzeń, które rozumie pętla. Kilka programów współpracuje
await
ze sobą, aż w końcu odbywa się wydarzenieawait
. To zdarzenie może komunikować się bezpośrednio z pętlą zdarzeń przezyield
sterowanie.Kluczem jest to, że standardowe zawieszenie umożliwia bezpośrednią komunikację pętli zdarzeń i zdarzeń. Pośredni stos programu coroutine nie wymaga żadnej wiedzy o tym, która pętla go uruchamia ani jak działają zdarzenia.
2.1.1. Wydarzenia w czasie
Najprostszym zdarzeniem do obsłużenia jest osiągnięcie punktu w czasie. Jest to również podstawowy blok kodu z wątkami: wątek jest powtarzany,
sleep
aż warunek zostanie spełniony. Jednak zwykłesleep
wykonywanie bloków samo w sobie - chcemy, aby inne programy nie były blokowane. Zamiast tego chcemy powiedzieć pętli zdarzeń, kiedy powinna wznowić bieżący stos programu.2.1.2. Definiowanie wydarzenia
Zdarzenie to po prostu wartość, którą możemy zidentyfikować - czy to poprzez wyliczenie, typ czy inną tożsamość. Możemy to zdefiniować za pomocą prostej klasy, która przechowuje nasz docelowy czas. Oprócz przechowywania informacji o wydarzeniach, możemy pozwolić
await
bezpośrednio na zajęcia.Ta klasa tylko przechowuje zdarzenie - nie mówi, jak właściwie je obsłużyć.
Jedyną specjalną cechą jest
__await__
to, czegoawait
szuka słowo kluczowe. W praktyce jest to iterator, ale nie jest dostępny dla zwykłych maszyn iteracyjnych.2.2.1. Oczekiwanie na wydarzenie
Teraz, gdy mamy wydarzenie, jak reagują na to programy? Powinniśmy być w stanie wyrazić odpowiednikiem
sleep
przezawait
ing naszą imprezę. Aby lepiej zobaczyć, co się dzieje, przez połowę czasu czekamy dwa razy:Możemy bezpośrednio utworzyć instancję i uruchomić tę procedurę. Podobnie jak w przypadku generatora, użycie
coroutine.send
powoduje uruchomienie programu aż do uzyskaniayield
wyniku.To daje nam dwa
AsyncSleep
zdarzenia, a następnieStopIteration
moment zakończenia programu. Zwróć uwagę, że jedyne opóźnienie pochodzi ztime.sleep
pętli! KażdyAsyncSleep
zapisuje tylko przesunięcie od bieżącego czasu.2.2.2. Wydarzenie + uśpienie
W tym momencie mamy do dyspozycji dwa odrębne mechanizmy:
AsyncSleep
Zdarzenia, które można wywołać z wnętrza programutime.sleep
które mogą czekać bez wpływu na programyWarto zauważyć, że te dwa są ortogonalne: żaden z nich nie wpływa ani nie uruchamia drugiego. W rezultacie możemy opracować własną strategię,
sleep
aby sprostać opóźnieniu związanemu z plikiemAsyncSleep
.2.3. Naiwna pętla wydarzeń
Jeśli mamy kilka programów, każdy może nam powiedzieć, kiedy chce się obudzić. Możemy wtedy zaczekać, aż pierwszy z nich będzie chciał wznowić, potem następny i tak dalej. Warto zauważyć, że w każdym punkcie zależy nam tylko na tym, który z nich będzie następny .
To sprawia, że planowanie jest proste:
Banalna implementacja nie wymaga żadnych zaawansowanych koncepcji. A
list
umożliwia sortowanie programów według daty. Czekanie jest normalnetime.sleep
. Uruchamianie programów działa tak samo jak wcześniej zcoroutine.send
.Oczywiście jest to dużo miejsca na ulepszenia. Możemy użyć sterty dla kolejki oczekiwania lub tabeli wysyłkowej dla zdarzeń. Moglibyśmy również pobrać wartości zwracane z programu
StopIteration
i przypisać je do programu. Jednak podstawowa zasada pozostaje ta sama.2.4. Czekanie w spółdzielni
AsyncSleep
Wydarzenie irun
pętla zdarzenie to wdrożenie w pełni robocze czasowe zdarzeń.To wspólnie przełącza się między każdym z pięciu programów, zawieszając każdy na 0,1 sekundy. Mimo że pętla zdarzeń jest synchroniczna, nadal wykonuje pracę w 0,5 sekundy zamiast 2,5 sekundy. Każdy program zachowuje stan i działa niezależnie.
3. Pętla zdarzeń we / wy
Pętla zdarzeń, która obsługuje,
sleep
jest odpowiednia do sondowania . Jednak oczekiwanie na I / O na uchwycie pliku może być wykonane bardziej wydajnie: system operacyjny implementuje I / O i wie, które uchwyty są gotowe. W idealnym przypadku pętla zdarzeń powinna obsługiwać jawne zdarzenie „gotowe do wejścia / wyjścia”.3.1.
select
wezwaniePython ma już interfejs do wysyłania zapytań do systemu operacyjnego w celu odczytania uchwytów we / wy. Gdy jest wywoływana z uchwytami do odczytu lub zapisu, zwraca uchwyty gotowe do odczytu lub zapisu:
Na przykład możemy
open
plik do zapisu i poczekać aż będzie gotowy:Po wybraniu zwraca,
writeable
zawiera nasz otwarty plik.3.2. Podstawowe zdarzenie we / wy
Podobnie jak w przypadku
AsyncSleep
żądania, musimy zdefiniować zdarzenie dla I / O. Zgodnie zselect
logiką bazową zdarzenie musi odnosić się do czytelnego obiektu - powiedzmy doopen
pliku. Ponadto przechowujemy, ile danych do odczytania.Podobnie jak w przypadku
AsyncSleep
, przechowujemy głównie dane wymagane dla podstawowego wywołania systemowego. Tym razem__await__
możliwe jest wielokrotne wznawianie - aż do przeczytania naszego pożądanegoamount
. Ponadto otrzymujemyreturn
wynik I / O zamiast po prostu wznawiać.3.3. Rozszerzanie pętli zdarzeń o odczyt we / wy
Podstawą naszej pętli zdarzeń jest nadal
run
zdefiniowana wcześniej. Najpierw musimy śledzić żądania odczytu. To nie jest już uporządkowany harmonogram, tylko mapujemy żądania odczytu do korektorów.Ponieważ
select.select
przyjmuje parametr timeout, możemy go użyć zamiasttime.sleep
.W ten sposób otrzymujemy wszystkie czytelne pliki - jeśli takie istnieją, uruchamiamy odpowiedni program. Jeśli ich nie ma, czekaliśmy wystarczająco długo na uruchomienie naszego obecnego programu.
Wreszcie, musimy faktycznie nasłuchiwać żądań odczytu.
3.4. Składając to razem
Powyższe było trochę uproszczeniem. Musimy trochę przejść, żeby nie głodować śpiących programów, jeśli zawsze potrafimy czytać. Musimy sobie poradzić, nie mając nic do czytania lub nie czekając. Jednak wynik końcowy nadal mieści się w 30 LOC.
3.5. Współpraca we / wy
Te
AsyncSleep
,AsyncRead
irun
implementacje są teraz w pełni funkcjonalny do snu i / lub przeczyta. Tak samo jak w przypadkusleepy
, możemy zdefiniować pomocnika do testowania czytania:Uruchamiając to, widzimy, że nasze I / O są przeplatane z oczekującym zadaniem:
4. Nieblokujące we / wy
Chociaż operacje we / wy na plikach są zrozumiałe, nie są one odpowiednie dla takich bibliotek, jak
asyncio
:select
wywołanie zawsze zwraca pliki i obaopen
iread
mogą blokować się na czas nieokreślony . To blokuje wszystkie procedury pętli zdarzeń - co jest złe. Biblioteki lubiąaiofiles
używają wątków i synchronizacji do fałszywego nieblokującego wejścia / wyjścia i zdarzeń w pliku.Jednak gniazda pozwalają na nieblokujące wejścia / wyjścia - a ich nieodłączne opóźnienie sprawia, że jest to znacznie bardziej krytyczne. W przypadku użycia w pętli zdarzeń oczekiwanie na dane i ponawianie próby można zawinąć bez blokowania czegokolwiek.
4.1. Nieblokujące zdarzenie we / wy
Podobnie jak w naszym przypadku
AsyncRead
, możemy zdefiniować zdarzenie wstrzymania i odczytu dla gniazd. Zamiast pobierać plik, bierzemy gniazdo - które musi być nieblokujące. Ponadto nasze__await__
zastosowaniasocket.recv
zamiastfile.read
.W przeciwieństwie do
AsyncRead
,__await__
wykonuje naprawdę nieblokujące operacje we / wy. Kiedy dane są dostępne, zawsze czyta. Gdy żadne dane nie są dostępne, zawiesza się zawsze . Oznacza to, że pętla zdarzeń jest blokowana tylko wtedy, gdy wykonujemy pożyteczną pracę.4.2. Odblokowanie pętli zdarzeń
Jeśli chodzi o pętlę zdarzeń, nic się nie zmienia. Zdarzenie do nasłuchiwania jest nadal takie samo jak w przypadku plików - deskryptor pliku oznaczony jako gotowy
select
.W tym momencie powinno być oczywiste, że
AsyncRead
iAsyncRecv
są to tego samego rodzaju wydarzenia. Moglibyśmy łatwo przekształcić je w jedno zdarzenie z wymiennym komponentem I / O. W efekcie pętla zdarzeń, procedury i zdarzenia wyraźnie oddzielają program planujący, dowolny kod pośredni i rzeczywiste operacje wejścia / wyjścia.4.3. Brzydka strona nieblokujących wejść / wyjść
Zasadniczo to, co powinieneś zrobić w tym momencie, to powtórzyć logikę
read
as arecv
forAsyncRecv
. Jednak teraz jest to o wiele bardziej brzydkie - musisz obsługiwać wczesne zwroty, gdy funkcje blokują się wewnątrz jądra, ale dają ci kontrolę. Na przykład otwarcie połączenia w porównaniu z otwarciem pliku jest znacznie dłuższe:Krótko mówiąc, pozostaje kilkadziesiąt wierszy obsługi wyjątków. W tym momencie zdarzenia i pętla zdarzeń już działają.
Uzupełnienie
Przykładowy kod na github
źródło
yield self
w AsyncSleep powodujeTask got back yield
błąd, dlaczego tak jest? Widzę, że kod w asyncio.Futures tego używa. Używanie czystej wydajności działa dobrze.Twoje
coro
usuwanie cukru jest koncepcyjnie poprawne, ale nieco niekompletne.await
nie zawiesza się bezwarunkowo, ale tylko wtedy, gdy napotka połączenie blokujące. Skąd wie, że połączenie jest blokowane? Decyduje o tym oczekiwany kod. Na przykład oczekiwana implementacja odczytu gniazda może zostać pozbawiona cukru:W prawdziwym asyncio kod równoważny modyfikuje stan a
Future
zamiast zwracać wartości magiczne, ale koncepcja jest taka sama. Po odpowiednim dostosowaniu do obiektu podobnego do generatora powyższy kod możnaawait
edytować.Po stronie dzwoniącego, gdy Twój program zawiera:
Rozpada się w coś bliskiego:
Osoby zaznajomione z generatorami mają tendencję do opisywania powyższego w kategoriach, w
yield from
których zawieszenie następuje automatycznie.Łańcuch zawieszenia jest kontynuowany aż do pętli zdarzeń, która zauważa, że program jest zawieszony, usuwa go z zestawu, który można uruchomić, i wykonuje programy, które można uruchomić, jeśli takie istnieją. Jeśli nie można uruchomić żadnego programu, pętla czeka,
select()
aż deskryptor pliku, którym program jest zainteresowany, stanie się gotowy do operacji we / wy. (Pętla zdarzeń zachowuje mapowanie deskryptora pliku do programu).W powyższym przykładzie, gdy
select()
powie pętli zdarzenia, którasock
jest czytelna, zostanie ona ponownie dodanacoro
do zestawu, który można uruchomić, więc będzie kontynuowana od punktu zawieszenia.Innymi słowy:
Domyślnie wszystko dzieje się w tym samym wątku.
Pętla zdarzeń jest odpowiedzialna za planowanie korektorów i budzenie ich, gdy cokolwiek czekały (zwykle wywołanie IO, które normalnie by się blokowało lub przekroczono limit czasu), staje się gotowe.
Aby dowiedzieć się więcej na temat pętli wydarzeń związanych z jazdą regularnie, polecam wykład Dave'a Beazleya, w którym demonstruje on kodowanie pętli wydarzenia od zera przed publicznością na żywo.
źródło
async.wait_for()
nie robi tego, co powinno ... Dlaczego dodanie wywołania zwrotnego do pętli zdarzeń i powiedzenie jej jest tak dużym problemem przetworzyć tyle wywołań zwrotnych, ile potrzeba, w tym to, które właśnie dodałeś? Moja frustracjaasyncio
wynika po części z faktu, że podstawowa koncepcja jest bardzo prosta i, na przykład, Emacs Lisp miał implementację od wieków, bez używania modnych słów ... (tj.create-async-process
Iaccept-process-output
- i to wszystko, co jest potrzebne ... (cd.)wait_for
nie robi tego, co powinien (robi, to procedura, na którą powinieneś czekać), chodzi o to, że twoje oczekiwania nie pasują do tego, do czego system został zaprojektowany i zaimplementowany. Myślę, że twój problem mógłby zostać dopasowany do asyncio, gdyby pętla zdarzeń działała w osobnym wątku, ale nie znam szczegółów twojego przypadku użycia i, szczerze mówiąc, twoje nastawienie nie sprawia, że pomaganie ci nie sprawia przyjemności.My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...
- Nic nie stoi na przeszkodzie, aby wdrożyć tę prostą koncepcję bez modnych słów dla Pythona :) Dlaczego w ogóle używasz tego brzydkiego asyncio? Wdrażaj własne od podstaw. Na przykład możesz zacząć od stworzenia własnejasync.wait_for()
funkcji, która robi dokładnie to, co powinna.asyncio
. Ale w zasadzie nie jest to moja decyzja. Jestem zmuszony do używania języka śmieciowego poprzez en.wikipedia.org/wiki/Ultimatum_game .Wszystko sprowadza się do dwóch głównych wyzwań, którym zajmuje się asyncio:
Odpowiedź na pierwszy punkt istnieje już od dłuższego czasu i nazywa się pętlą wyboru . W Pythonie jest zaimplementowany w module selektorów .
Drugie pytanie wiąże się z koncepcją coroutine , czyli funkcji, które mogą zatrzymać ich wykonanie i zostać przywrócone później. W Pythonie programy są implementowane przy użyciu generatorów i instrukcji yield from . To właśnie kryje się za składnią async / await .
Więcej zasobów w tej odpowiedzi .
EDYCJA: adresowanie twojego komentarza na temat goroutines:
Najbliższy odpowiednik gorutyny w asyncio w rzeczywistości nie jest coroutine, ale zadaniem (zobacz różnicę w dokumentacji ). W Pythonie program (lub generator) nie wie nic o pojęciach pętli zdarzeń lub I / O. Jest to po prostu funkcja, która może zatrzymać swoje wykonanie
yield
, zachowując swój bieżący stan, dzięki czemu można ją później przywrócić.yield from
Składnia umożliwia łączenia ich w przejrzysty sposób.Teraz, w ramach zadania asyncio, program na samym dole łańcucha zawsze kończy się określeniem przyszłości . Ta przyszłość pędzi do pętli zdarzeń i zostaje zintegrowana z wewnętrzną maszynerią. Gdy przyszłość jest ustawiona na wykonanie przez inne wewnętrzne wywołanie zwrotne, pętla zdarzeń może przywrócić zadanie, wysyłając przyszłość z powrotem do łańcucha coroutine.
EDYCJA: Odpowiadanie na niektóre pytania w Twoim poście:
Nie, nic się nie dzieje w wątku. We / wy jest zawsze zarządzane przez pętlę zdarzeń, głównie poprzez deskryptory plików. Jednak rejestracja tych deskryptorów plików jest zwykle ukryta przez programy wysokiego poziomu, co czyni dla ciebie brudną robotę.
I / O to każde wywołanie blokujące. W asyncio wszystkie operacje we / wy powinny przechodzić przez pętlę zdarzeń, ponieważ, jak powiedziałeś, pętla zdarzeń nie ma sposobu, aby być świadomym, że wywołanie blokujące jest wykonywane w pewnym kodzie synchronicznym. Oznacza to, że nie powinieneś używać synchronicznego
open
w kontekście coroutine. Zamiast tego użyj dedykowanej biblioteki, takiej jak aiofiles, która zapewnia asynchroniczną wersjęopen
.źródło
yield from
tak naprawdę nic nie mówi.yield from
to tylko konstrukcja składni, a nie fundamentalny element konstrukcyjny, który mogą wykonywać komputery. Podobnie w przypadku pętli wyboru. Tak, programy w Go również używają pętli wyboru, ale to, co próbowałem zrobić, zadziałałoby w Go, ale nie działa w Pythonie. Potrzebuję bardziej szczegółowych odpowiedzi, aby zrozumieć, dlaczego to nie działa.asyncio
dla mnie oznacza, sprowadziłoby się do kodu C, który ilustruje, na co przetłumaczono składnię Pythona.