Dlaczego potrzebujemy słowa kluczowego asynchronicznego?

19

Właśnie zacząłem bawić się w async / czekaj w .Net 4.5. Na początku jestem ciekawy, dlaczego słowo kluczowe asynchroniczne jest potrzebne? Wyjaśnienie, które przeczytałem było takie, że jest to marker, więc kompilator wie, że metoda na coś czeka. Ale wydaje się, że kompilator powinien być w stanie to zrozumieć bez słowa kluczowego. Co jeszcze to robi?

ConditionRacer
źródło
2
Oto naprawdę dobry artykuł (seria) na blogu, który szczegółowo opisuje słowo kluczowe w języku C # i powiązane funkcje w języku F #.
Paul
Myślę, że jest to głównie marker dla dewelopera, a nie kompilatora.
CodesInChaos
@svick dzięki, tego właśnie szukałem. Ma teraz sens.
ConditionRacer

Odpowiedzi:

23

Jest tu kilka odpowiedzi i wszystkie mówią o tym, co robią metody asynchroniczne, ale żadna z nich nie odpowiada na pytanie, dlatego asyncjest potrzebne jako słowo kluczowe, które znajduje się w deklaracji funkcji.

To nie jest „polecenie kompilatorowi, aby przekształcił funkcję w specjalny sposób”; awaitsam mógł to zrobić. Dlaczego? Ponieważ C # ma już inny mechanizm, gdzie obecność specjalnego słowa kluczowego w treści metody powoduje kompilator wykonać skrajny (i bardzo podobna do async/await) transformacje na korpusie metodą: yield.

Tyle że yieldnie jest to własne słowo kluczowe w języku C # i zrozumienie, dlaczego to wyjaśni async. W przeciwieństwie do większości języków obsługujących ten mechanizm, w języku C # nie można powiedzieć, yield value;że trzeba powiedzieć yield return value;. Dlaczego? Ponieważ został dodany do języka po tym, jak C # już istniał, i całkiem rozsądne było założenie, że ktoś gdzieś mógł użyć yieldjako nazwy zmiennej. Ponieważ jednak nie istniał wcześniejszy scenariusz, w którym <variable name> returnbyłby poprawny składni, yield returndodano do języka, aby umożliwić wprowadzenie generatorów przy jednoczesnym zachowaniu 100% wstecznej zgodności z istniejącym kodem.

I dlatego asynczostał dodany jako modyfikator funkcji: aby uniknąć zepsucia istniejącego kodu, który był używany awaitjako nazwa zmiennej. Ponieważ nie asyncistniały już żadne metody, stary kod nie jest unieważniany, aw nowym kodzie kompilator może wykorzystać obecność asyncznacznika, aby wiedzieć, że awaitnależy go traktować jako słowo kluczowe, a nie identyfikator.

Mason Wheeler
źródło
3
Tak też mówi ten artykuł (pierwotnie opublikowany przez @svick w komentarzach), ale nikt nigdy nie opublikował go jako odpowiedzi. Zajęło to tylko dwa i pół roku! Dzięki :)
ConditionRacer
Czy to nie oznacza, że ​​w niektórych przyszłych wersjach asyncmożna zrezygnować z wymogu podania słowa kluczowego? Oczywiście byłby to przełom BC, ale można by pomyśleć, że wpłynie na bardzo niewiele projektów i będzie prostym wymogiem aktualizacji (tj. Zmiana nazwy zmiennej).
Pytania do Kwolonela,
6

zmienia metodę z normalnej na obiekt z wywołaniem zwrotnym, co wymaga zupełnie innego podejścia do generowania kodu

a kiedy dzieje się coś tak drastycznego, zwyczajowo wyraźnie to zaznacza się (nauczyliśmy się tej lekcji od C ++)

maniak zapadkowy
źródło
Czy to naprawdę coś dla czytelnika / programisty, a nie dla kompilatora?
ConditionRacer
@ Justin984 zdecydowanie pomaga zasygnalizować kompilatorowi, że musi wydarzyć się coś specjalnego
maniak ratchet
Chyba jestem ciekawy, dlaczego kompilator tego potrzebuje. Czy nie może po prostu przeanalizować metody i zobaczyć, czy gdzieś w treści metody jest oczekiwanie?
ConditionRacer
pomaga, gdy chcesz zaplanować wiele asyncs jednocześnie, aby nie działały jeden po drugim, ale jednocześnie (jeśli wątki są przynajmniej dostępne)
maniak ratchet
@ Justin984: Nie każda zwracana metoda Task<T>faktycznie ma charakterystykę asyncmetody - na przykład możesz po prostu przejrzeć logikę biznesową / fabryczną, a następnie przekazać ją innej powracającej metodzie Task<T>. asyncmetody mają typ zwracany Task<T>w podpisie, ale tak naprawdę nie zwracają a Task<T>, po prostu zwracają a T. Aby to wszystko zrozumieć bez asyncsłowa kluczowego, kompilator C # musiałby przeprowadzić różnego rodzaju głęboką inspekcję metody, co prawdopodobnie spowolniłoby ją nieco i doprowadziłoby do wszelkich niejasności.
Aaronaught
4

Cały pomysł ze słowami kluczowymi takimi jak „asynchroniczny” lub „niebezpieczny” polega na usunięciu niejednoznaczności co do sposobu traktowania modyfikowanego przez nich kodu. W przypadku słowa kluczowego async informuje kompilator, aby traktował zmodyfikowaną metodę jako coś, co nie musi natychmiast wracać. Pozwala to wątkowi, w którym ta metoda jest używana, kontynuować bez konieczności oczekiwania na wyniki tej metody. To skutecznie optymalizacja kodu.

Inżynier świata
źródło
Mam pokusę, aby głosować za odrzuceniem, ponieważ chociaż pierwszy akapit jest poprawny, porównanie z przerwaniami jest niepoprawne (moim zdaniem).
Paul
Widzę je jako nieco analogiczne pod względem celu, ale usunę je ze względu na jasność.
Inżynier świata
Przerwania są w mojej głowie bardziej jak zdarzenia niż kod asynchroniczny - powodują zmianę kontekstu i uruchamiają się do końca przed wznowieniem normalnego kodu (a normalny kod może być całkowicie nieświadomy jego działania), podczas gdy w przypadku kodu asynchronicznego metoda może nie zostały zakończone przed wznowieniem wątku wywołującego. Rozumiem, co próbowaliście powiedzieć, ale o różnych mechanizmach wymagających różnych implementacji pod maską, ale przeszkody po prostu mi nie przeszkadzały, aby zilustrować tę kwestię. (+1 za resztę, btw.)
Paweł
0

OK, oto moje zdanie na ten temat.

Istnieje coś zwanego coroutines, które jest znane od dziesięcioleci. („Knuth and Hopper” -class „od dziesięcioleci”) Są uogólnieniami podprogramów , w tym, że nie tylko uzyskują i zwalniają kontrolę przy instrukcji początkowej i zwrotnej funkcji, ale także robią to w określonych punktach ( punktach zawieszenia ). Podprogram jest korupcją bez punktów zawieszenia.

Są ŁATWE w implementacji z makrami C, jak pokazano w poniższym artykule na temat „protothreads”. ( http://dunkels.com/adam/dunkels06protothreads.pdf ) Przeczytaj. Poczekam...

Najważniejsze jest to, że makra tworzą duże switchi caseetykiety w każdym punkcie zawieszenia. W każdym punkcie zawieszenia funkcja przechowuje wartość caseetykiety bezpośrednio po niej , dzięki czemu wie, gdzie wznowić wykonywanie przy następnym wywołaniu. I zwraca kontrolę dzwoniącemu.

Odbywa się to bez modyfikowania pozornego przepływu kontroli kodu opisanego w „protothread”.

Wyobraź sobie teraz, że masz dużą pętlę, która z kolei nazywa wszystkie te „protothreads” i jednocześnie możesz wykonywać „protothreads” w jednym wątku.

To podejście ma dwie wady:

  1. Nie można zachować stanu w zmiennych lokalnych między wznowieniami.
  2. Nie można zawiesić „protothread” z dowolnej głębokości połączenia. (wszystkie punkty zawieszenia muszą znajdować się na poziomie 0)

Istnieją obejścia dla obu:

  1. Wszystkie zmienne lokalne muszą zostać wyciągnięte do kontekstu protothread (kontekst, który jest już potrzebny, ponieważ protothread musi przechowywać swój następny punkt wznowienia)
  2. Jeśli czujesz, że naprawdę musisz zadzwonić z innego protothreada z „protothread”, „spawn” dziecięcy protothread i zawieś go do czasu ukończenia go.

A jeśli posiadasz wsparcie kompilatora do wykonywania operacji przepisywania, które wykonują makra i obejście, cóż, możesz po prostu napisać swój protothread code tak, jak chcesz i wstawić punkty zawieszenia słowem kluczowym.

I to asynci awaitto chodzi: tworzenie (Stackless) współprogram.

Korpusy w C # są reifikowane jako obiekty klasy (ogólnej lub nie-ogólnej) Task.

Uważam te słowa kluczowe za bardzo mylące. Moje mentalne czytanie to:

  • async jako „zawieszalny”
  • await jako „zawieś do zakończenia”
  • Task jako „przyszłość…”

Teraz. Czy naprawdę musimy zaznaczyć tę funkcję async? Oprócz powiedzenia, że ​​powinien uruchomić mechanizmy przepisywania kodu, aby funkcja stała się coroutine, rozwiązuje pewne niejasności. Rozważ ten kod.

public Task<object> AmIACoroutine() {
    var tcs = new TaskCompletionSource<object>();
    return tcs.Task;
}

Zakładając, że asyncnie jest to obowiązkowe, czy jest to korupcja czy normalna funkcja? Czy kompilator powinien przepisać go jako coroutine, czy nie? Oba mogłyby być możliwe z inną ewentualną semantyką.

Laurent LA RIZZA
źródło