Po pierwsze, dziękuję za miłe słowa. To naprawdę niesamowita funkcja i cieszę się, że mogłem być jej małą częścią.
Jeśli cały mój kod powoli zmienia się na asynchroniczny, dlaczego nie ustawić go domyślnie jako asynchronicznego?
Cóż, przesadzasz; cały kod nie zmienia się w asynchroniczny. Kiedy dodajesz razem dwie „zwykłe” liczby całkowite, nie czekasz na wynik. Kiedy dodasz razem dwie przyszłe liczby całkowite, aby otrzymać trzecią przyszłą liczbę całkowitą - ponieważ to Task<int>
jest liczba całkowita, do której uzyskasz dostęp w przyszłości - oczywiście prawdopodobnie będziesz czekać na wynik.
Głównym powodem, dla którego nie należy tworzyć wszystkich elementów asynchronicznych, jest to, że celem async / await jest ułatwienie pisania kodu w świecie z wieloma operacjami o dużym opóźnieniu . Zdecydowana większość twoich operacji nie ma dużych opóźnień, więc nie ma sensu przyjmować spadku wydajności, który zmniejsza to opóźnienie. Raczej, kilka kluczowych z twoich operacji ma duże opóźnienia, a te operacje powodują infekcję asynchronicznych zombie w całym kodzie.
jeśli wydajność jest jedynym problemem, z pewnością sprytne optymalizacje mogą automatycznie usunąć narzut, gdy nie jest potrzebny.
W teorii teoria i praktyka są podobne. W praktyce nigdy nie są.
Pozwólcie, że przedstawię trzy punkty przeciwko tego rodzaju transformacji, po której następuje optymalizacja.
Pierwsza kwestia to: asynchronizacja w C # / VB / F # jest zasadniczo ograniczoną formą przekazywania kontynuacji . Ogromna ilość badań przeprowadzona w społeczności języków funkcjonalnych zajęła się ustaleniem sposobów optymalizacji kodu, który w dużym stopniu wykorzystuje styl przekazywania kontynuacji. Zespół kompilatora prawdopodobnie musiałby rozwiązać bardzo podobne problemy w świecie, w którym „asynchroniczna” była wartością domyślną, a metody nie-asynchroniczne musiałyby zostać zidentyfikowane i de-asynchroniczne. Zespół C # nie jest tak naprawdę zainteresowany rozwiązywaniem otwartych problemów badawczych, więc to duże punkty przeciwko temu.
Po drugie, C # nie ma takiego poziomu „przejrzystości referencyjnej”, który sprawia, że tego rodzaju optymalizacje są łatwiejsze do wykonania. Przez „przezroczystość referencyjną” rozumiem właściwość, od której wartość wyrażenia nie zależy, kiedy jest oceniane . Wyrażenia takie jak 2 + 2
są referencyjnie przejrzyste; jeśli chcesz, możesz przeprowadzić ocenę w czasie kompilacji lub odłożyć ją do czasu wykonania i uzyskać tę samą odpowiedź. Ale wyrażenia takiego jak x+y
nie można przesuwać w czasie, ponieważ x i y mogą się zmieniać w czasie .
Async znacznie utrudnia rozumowanie, kiedy wystąpi efekt uboczny. Przed asynchronizacją, jeśli powiedziałeś:
M();
N();
i M()
był void M() { Q(); R(); }
i N()
był void N() { S(); T(); }
, a R
i S
skutki uboczne produkty, to wiesz, że efektem ubocznym R w stanie przed efektem ubocznym S jest. Ale jeśli masz async void M() { await Q(); R(); }
to nagle to wychodzi przez okno. Nie masz żadnej gwarancji, czy R()
wydarzy się to przed, czy po S()
(chyba że oczywiście M()
jest to oczekiwane; ale oczywiście Task
nie trzeba czekać na to N()
).
Teraz wyobraź sobie, że ta właściwość, że nie wiadomo już, w jakiej kolejności występują efekty uboczne, dotyczy każdego fragmentu kodu w twoim programie, z wyjątkiem tych, które optymalizator zdoła usunąć z asynchronizacji. Zasadniczo nie masz już pojęcia, które wyrażenia będą oceniane w jakiej kolejności, co oznacza, że wszystkie wyrażenia muszą być referencyjnie przezroczyste, co jest trudne w języku takim jak C #.
Trzecią kwestią jest to, że musisz zapytać „dlaczego asynchronizacja jest tak wyjątkowa?” Jeśli masz zamiar argumentować, że każda operacja powinna być faktycznie Task<T>
a, to musisz umieć odpowiedzieć na pytanie „dlaczego nie Lazy<T>
?” lub „dlaczego nie Nullable<T>
?” lub „dlaczego nie IEnumerable<T>
?” Ponieważ mogliśmy to równie łatwo zrobić. Dlaczego nie miałoby być tak, że każda operacja jest podnoszona do wartości zerowej ? Albo każda operacja jest leniwie obliczana, a wynik jest zapisywany w pamięci podręcznej na później , lub wynik każdej operacji jest sekwencją wartości zamiast pojedynczej wartości . Następnie musisz spróbować zoptymalizować te sytuacje, w których wiesz, że „och, to nigdy nie może być zerowe, więc mogę wygenerować lepszy kod” i tak dalej.
Chodzi o to, że nie jest dla mnie jasne, Task<T>
czy w rzeczywistości jest to takie szczególne uzasadnienie tak dużej ilości pracy.
Jeśli tego rodzaju rzeczy Cię interesują, polecam zbadanie języków funkcjonalnych, takich jak Haskell, które mają znacznie większą przejrzystość referencyjną i pozwalają na wszelkiego rodzaju ocenę poza kolejnością i automatyczne buforowanie. Haskell ma również znacznie silniejsze wsparcie w swoim systemie typów dla tego rodzaju „monadycznych podnoszenia”, o których wspomniałem.