Czy to hybryda? (np. czy mój program .NET używa stosu, dopóki nie trafi w wywołanie asynchroniczne, a następnie przełącza się na inną strukturę aż do ukończenia, w którym momencie stos jest rozwijany z powrotem do stanu, w którym może być pewien kolejnych elementów itp.? )
Zasadniczo tak.
Załóżmy, że mamy
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
Oto niezwykle uproszczone wyjaśnienie, w jaki sposób kontynuowane są kontynuacje. Rzeczywisty kod jest znacznie bardziej złożony, ale ten pomysł przenika.
Kliknij przycisk. Wiadomość jest w kolejce. Pętla komunikatów przetwarza wiadomość i wywołuje moduł obsługi kliknięć, umieszczając na stosie adres zwrotny kolejki komunikatów. Oznacza to, że po zakończeniu procedury obsługi pętla komunikatów musi nadal działać. Tak więc kontynuacją programu obsługi jest pętla.
Moduł obsługi kliknięć wywołuje Foo (), umieszczając adres zwrotny na stosie. Oznacza to, że kontynuacja Foo jest pozostałą częścią procedury obsługi kliknięć.
Foo wywołuje Task.Delay, umieszczając swój adres zwrotny na stosie.
Task.Delay robi wszystko, co trzeba, aby natychmiast zwrócić zadanie. Stos jest wyskakujący i wróciliśmy do Foo.
Foo sprawdza zwrócone zadanie, aby sprawdzić, czy zostało zakończone. Nie jest. Kontynuacja tego czekają na to, aby zadzwonić Blah (), więc Foo tworzy delegata, który wywołuje Blah (), a sygnały, że delegować się jako kontynuacja zadania. (Właśnie popełniłem małe nieporozumienie; złapałeś go? Jeśli nie, ujawnimy to za chwilę.)
Następnie Foo tworzy własny obiekt Task, oznacza go jako niekompletny i zwraca go stosowi do programu obsługi kliknięć.
Moduł obsługi kliknięć sprawdza zadanie Foo i odkrywa, że jest ono niekompletne. Kontynuacją oczekiwania w module obsługi jest wywołanie Bar (), więc moduł obsługi kliknięć tworzy delegata, który wywołuje Bar () i ustawia go jako kontynuację zadania zwróconego przez Foo (). Następnie zwraca stos do pętli komunikatów.
Pętla komunikatów przetwarza wiadomości. Ostatecznie magia timera utworzona przez zadanie opóźnienia robi swoje i wysyła komunikat do kolejki, informujący, że kontynuacja zadania opóźnienia może być teraz wykonana. Pętla komunikatów wywołuje więc kontynuację zadania, jak zwykle umieszczając się na stosie. Ten delegat nazywa Blah (). Blah () robi to, co robi i zwraca stos.
Co się teraz stanie? Oto trudny kawałek. Kontynuacja zadania opóźnienia nie tylko wywołuje Blah (). Musi także wywołać wywołanie Bar () , ale to zadanie nie wie o Bar!
Foo rzeczywiście stworzył delegata, że (1) wywołuje Blah () i (2) wywołuje kontynuację zadania, które Foo stworzył i oddał do obsługi zdarzeń. W ten sposób nazywamy delegata, który wywołuje Bar ().
A teraz zrobiliśmy wszystko, co musieliśmy zrobić, we właściwej kolejności. Ale nigdy nie przestawaliśmy przetwarzać wiadomości w pętli komunikatów na bardzo długo, więc aplikacja pozostała responsywna.
To, że te scenariusze są zbyt zaawansowane dla stosu, ma sens, ale co zastępuje stos?
Wykres obiektów zadań zawierający odniesienia do siebie za pośrednictwem klas zamknięcia delegatów. Te klasy zamknięcia są automatami państwowymi, które śledzą pozycję ostatnio wykonywanego czekania i wartości miejscowych. Dodatkowo w podanym przykładzie kolejka działań globalnych realizowana przez system operacyjny oraz pętla komunikatów, która wykonuje te akcje.
Ćwiczenie: jak myślisz, jak to wszystko działa w świecie bez pętli wiadomości? Na przykład aplikacje konsolowe. Oczekiwanie w aplikacji na konsolę jest zupełnie inne; czy możesz wywnioskować, jak to działa na podstawie tego, co wiesz do tej pory?
Kiedy dowiedziałem się o tym lata temu, stos był tam, ponieważ był błyskawiczny i lekki, kawałek pamięci przydzielonej przy aplikacji z dala od sterty, ponieważ wspierał wysoce wydajne zarządzanie dla danego zadania (zamierzona gra słów?). Co się zmieniło
Stosy są użyteczną strukturą danych, gdy czasy życia aktywacji metod tworzą stos, ale w moim przykładzie aktywacje modułu obsługi kliknięć, Foo, Bar i Blah nie tworzą stosu. Dlatego struktura danych reprezentująca ten przepływ pracy nie może być stosem; raczej jest to wykres zadań przydzielonych do sterty i delegatów, który reprezentuje przepływ pracy. Oczekiwania to punkty w przepływie pracy, w których nie można poczynić dalszych postępów w przepływie pracy, dopóki prace rozpoczęte wcześniej nie zostaną zakończone; podczas gdy czekamy, możemy wykonać inną pracę, która nie zależy od zakończenia tych konkretnych rozpoczętych zadań.
Stos to po prostu tablica ramek, w których ramki zawierają (1) wskaźniki do środka funkcji (gdzie nastąpiło wywołanie) i (2) wartości zmiennych lokalnych i temps. Kontynuacja zadań jest taka sama: delegat jest wskaźnikiem do funkcji i ma stan, który odwołuje się do określonego punktu na środku funkcji (gdzie nastąpiło oczekiwanie), a zamknięcie zawiera pola dla każdej zmiennej lokalnej lub tymczasowej . Ramki po prostu nie tworzą już ładnej, uporządkowanej tablicy, ale wszystkie informacje są takie same.