Odpowiem na Twoje konkretne pytania poniżej, ale prawdopodobnie po prostu przeczytasz moje obszerne artykuły o tym, jak zaprojektowaliśmy wydajność i czekanie.
https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/
https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/
https://blogs.msdn.microsoft.com/ericlippert/tag/async/
Niektóre z tych artykułów są obecnie nieaktualne; wygenerowany kod różni się pod wieloma względami. Ale to z pewnością da ci wyobrażenie, jak to działa.
Ponadto, jeśli nie rozumiesz, w jaki sposób lambdy są generowane jako klasy zamknięcia, zrozum to najpierw . Nie zrobisz asynchronicznych głów ani ogonów, jeśli nie masz wyłączonych lambd.
Po osiągnięciu czasu oczekiwania, skąd środowisko wykonawcze wie, który fragment kodu powinien zostać wykonany jako następny?
await
jest generowany jako:
if (the task is not completed)
assign a delegate which executes the remainder of the method as the continuation of the task
return to the caller
else
execute the remainder of the method now
W zasadzie to wszystko. Oczekiwanie to tylko fantazyjny powrót.
Skąd wie, kiedy może wznowić od miejsca, w którym zostało przerwane, i jak zapamiętuje gdzie?
Jak to zrobić bez czekania? Kiedy metoda foo wywołuje metodę bar, w jakiś sposób pamiętamy, jak wrócić do środka foo, z nienaruszonymi wszystkimi lokalnymi lokalizacjami aktywacji foo, bez względu na to, co robi bar.
Wiesz, jak to się robi w asemblerze. Rekord aktywacji dla foo jest umieszczany na stosie; zawiera wartości miejscowych. W momencie wywołania adres zwrotny w foo jest umieszczany na stosie. Kiedy pasek jest gotowy, wskaźnik stosu i wskaźnik instrukcji są resetowane do miejsca, w którym powinny być, a foo kontynuuje pracę od miejsca, w którym zostało przerwane.
Kontynuacja await jest dokładnie taka sama, z wyjątkiem tego, że rekord jest umieszczany na stercie z oczywistego powodu, że sekwencja aktywacji nie tworzy stosu .
Delegat, który await podaje jako kontynuację zadania, zawiera (1) liczbę będącą danymi wejściowymi do tabeli przeglądowej, która zawiera wskaźnik instrukcji, którą należy wykonać w następnej kolejności, oraz (2) wszystkie wartości lokalnych i tymczasowych.
Jest tam trochę dodatkowego sprzętu; na przykład w .NET niedozwolone jest rozgałęzianie się w środku bloku try, więc nie można po prostu wstawić adresu kodu wewnątrz bloku try do tabeli. Ale to są szczegóły księgowe. Koncepcyjnie rekord aktywacji jest po prostu przenoszony na stos.
Co dzieje się z aktualnym stosem wywołań, czy zostaje on w jakiś sposób zapisany?
Odpowiednie informacje w aktualnym rekordzie aktywacji nigdy nie są umieszczane na stosie w pierwszej kolejności; jest on przydzielany na stosie od samego początku. (Cóż, parametry formalne są normalnie przekazywane na stos lub w rejestrach, a następnie kopiowane do lokalizacji sterty, gdy rozpoczyna się metoda).
Rekordy aktywacji dzwoniących nie są przechowywane; pamiętaj, że oczekiwanie prawdopodobnie do nich wróci, więc będą normalnie traktowani.
Zauważ, że jest to zasadnicza różnica między uproszczonym stylem przechodzenia kontynuacji await a strukturami prawdziwego wezwania z bieżącą kontynuacją, które widzisz w językach takich jak Scheme. W tych językach cała kontynuacja, w tym kontynuacja z powrotem do dzwoniących, jest przechwytywana przez call-cc .
Co się stanie, jeśli metoda wywołująca wywoła inne metody, zanim zaczeka - dlaczego stos nie zostanie nadpisany?
Te wywołania metod powracają, więc ich rekordy aktywacji nie są już na stosie w punkcie oczekiwania.
I jak, u licha, środowisko wykonawcze przeszłoby przez to wszystko w przypadku wyjątku i rozwinięcia stosu?
W przypadku nieprzechwyconego wyjątku wyjątek jest przechwytywany, zapisywany w zadaniu i ponownie generowany po pobraniu wyniku zadania.
Pamiętasz całą księgowość, o której wspomniałem wcześniej? Pozwólcie, że powiem wam, że uzyskanie prawidłowej semantyki wyjątków było ogromnym problemem.
Po osiągnięciu plonu, w jaki sposób środowisko wykonawcze śledzi punkt, w którym należy zebrać rzeczy? Jak zachowywany jest stan iteratora?
Ta sama droga. Stan miejscowych jest przenoszony na stertę, a liczba reprezentująca instrukcję, która MoveNext
powinna zostać wznowiona przy następnym wywołaniu, jest przechowywana razem z lokalnymi.
I znowu, w bloku iteratora znajduje się kilka narzędzi, aby upewnić się, że wyjątki są obsługiwane poprawnie.