Asynchroniczny-Oczekujcie wzór .net 4.5 jest zmiana paradygmatu. To prawie zbyt piękne, aby mogło być prawdziwe.
Przeportowałem kod obciążony we / wy do async-await, ponieważ blokowanie to już przeszłość.
Sporo osób porównuje asynchroniczne oczekiwanie do plagi zombie i okazało się, że jest to raczej trafne. Kod asynchroniczny lubi inny kod asynchroniczny (potrzebujesz funkcji asynchronicznej, aby czekać na funkcję asynchroniczną). Dlatego coraz więcej funkcji staje się asynchronicznych, a to stale rośnie w twojej bazie kodu.
Zmiana funkcji na asynchroniczną jest nieco powtarzalną i pozbawioną wyobraźni pracą. Wrzuć async
słowo kluczowe do deklaracji, zawiń wartość zwracaną przez Task<>
i gotowe. To raczej niepokojące, jak łatwy jest cały proces, a wkrótce skrypt zastępujący tekst zautomatyzuje dla mnie większość „portowania”.
A teraz pytanie… Jeśli cały mój kod powoli zmienia się w asynchroniczny, dlaczego nie ustawić go domyślnie jako asynchronicznego?
Zakładam, że oczywistym powodem jest wydajność. Async-await ma swój narzut i kod, który nie musi być asynchroniczny, a najlepiej nie powinien. Ale jeśli jedynym problemem jest wydajność, z pewnością sprytne optymalizacje mogą automatycznie usunąć narzut, gdy nie jest potrzebny. Czytałem o optymalizacji „szybkiej ścieżki” i wydaje mi się, że sama powinna zająć się większością.
Być może jest to porównywalne do zmiany paradygmatu wprowadzonej przez śmieciarzy. We wczesnych dniach GC uwalnianie własnej pamięci było zdecydowanie bardziej wydajne. Ale masy nadal wybierają automatyczne zbieranie na rzecz bezpieczniejszego, prostszego kodu, który może być mniej wydajny (a nawet to prawdopodobnie nie jest już prawdą). Może tak powinno być w tym przypadku? Dlaczego nie wszystkie funkcje powinny być asynchroniczne?
źródło
async
(aby spełnić jakiś kontrakt), jest to prawdopodobnie zły pomysł. Otrzymujesz wady asynchroniczności (zwiększony koszt wywołań metod; konieczność użyciaawait
w kodzie wywołującym), ale nie ma żadnych zalet.Odpowiedzi:
Po pierwsze, dziękuję za miłe słowa. To naprawdę niesamowita funkcja i cieszę się, że mogłem być jej małą częścią.
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.
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 jakx+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ś:
i
M()
byłvoid M() { Q(); R(); }
iN()
byłvoid N() { S(); T(); }
, aR
iS
skutki uboczne produkty, to wiesz, że efektem ubocznym R w stanie przed efektem ubocznym S jest. Ale jeśli maszasync void M() { await Q(); R(); }
to nagle to wychodzi przez okno. Nie masz żadnej gwarancji, czyR()
wydarzy się to przed, czy poS()
(chyba że oczywiścieM()
jest to oczekiwane; ale oczywiścieTask
nie trzeba czekać na toN()
).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 nieLazy<T>
?” lub „dlaczego nieNullable<T>
?” lub „dlaczego nieIEnumerable<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.
źródło
async/await
ponieważ coś ukrytego pod warstwami abstrakcji może być asynchroniczne.Jak wspomniałeś, wydajność jest jednym z powodów. Zwróć uwagę, że opcja „szybkiej ścieżki”, z którą utworzono łącze, poprawia wydajność w przypadku ukończonego zadania, ale nadal wymaga znacznie więcej instrukcji i narzutu w porównaniu do pojedynczego wywołania metody. W związku z tym, nawet z „szybką ścieżką”, dodajesz dużo złożoności i narzutów przy każdym wywołaniu metody asynchronicznej.
Kompatybilność wsteczna, a także kompatybilność z innymi językami (w tym scenariuszami międzyoperacyjnymi) również stałaby się problematyczna.
Drugi to kwestia złożoności i zamiaru. Operacje asynchroniczne zwiększają złożoność - w wielu przypadkach funkcje języka to ukrywają, ale jest wiele przypadków, w których tworzenie metod
async
zdecydowanie zwiększa złożoność ich użycia. Jest to szczególnie ważne, jeśli nie masz kontekstu synchronizacji, ponieważ metody asynchroniczne mogą łatwo spowodować nieoczekiwane problemy z wątkami.Ponadto istnieje wiele procedur, które z natury nie są asynchroniczne. Są one bardziej sensowne jako operacje synchroniczne. Na przykład zmuszanie
Math.Sqrt
do byciaTask<double> Math.SqrtAsync
byłoby śmieszne, ponieważ nie ma żadnego powodu, aby było to asynchroniczne. Zamiastasync
przepychać swoją aplikację, skończyłbyś zawait
rozprzestrzenianiem się wszędzie .Spowodowałoby to również całkowite zerwanie obecnego paradygmatu, a także spowodowałoby problemy z właściwościami (które w rzeczywistości są po prostu parami metod ... czy one też byłyby asynchroniczne?) I miałyby inne konsekwencje w projektowaniu frameworka i języka.
Jeśli wykonujesz dużo pracy związanej z IO, zauważysz, że
async
wszechobecne używanie jest świetnym dodatkiem, wiele z twoich procedur będzieasync
. Jednak gdy zaczynasz wykonywać pracę związaną z procesorem, generalnie tworzenie rzeczyasync
nie jest dobre - ukrywa fakt, że używasz cykli procesora w ramach interfejsu API, który wydaje się być asynchroniczny, ale tak naprawdę niekoniecznie jest naprawdę asynchroniczny.źródło
await FooAsync()
prostsze niżFoo()
? I zamiast czasami małego efektu domina, cały czas masz ogromny efekt domina i nazywasz to poprawą?Pomijając wydajność - asynchronizacja może wiązać się z kosztami produktywności. Na kliencie (WinForms, WPF, Windows Phone) jest to dobrodziejstwo dla produktywności. Ale na serwerze lub w innych scenariuszach bez interfejsu użytkownika płacisz za produktywność. Na pewno nie chcesz tam domyślnie korzystać z asynchronii. Użyj go, gdy potrzebujesz zalet skalowalności.
Użyj go, gdy jesteś w najlepszym miejscu. W innych przypadkach nie rób tego.
źródło
Uważam, że istnieje dobry powód, aby wszystkie metody były asynchroniczne, jeśli nie są do tego potrzebne - rozszerzalność. Selektywne tworzenie metod asynchronicznych działa tylko wtedy, gdy twój kod nigdy nie ewoluuje i wiesz, że metoda A () jest zawsze związana z procesorem (utrzymujesz synchronizację), a metoda B () jest zawsze związana we / wy (oznaczasz ją jako asynchroniczną).
Ale co, jeśli coś się zmieni? Tak, A () wykonuje obliczenia, ale w pewnym momencie w przyszłości trzeba było dodać tam rejestrowanie, raportowanie lub wywołanie zwrotne zdefiniowane przez użytkownika z implementacją, która nie może przewidzieć, lub algorytm został rozszerzony i obejmuje teraz nie tylko obliczenia procesora, ale też jakieś I / O? Będziesz musiał przekonwertować metodę na asynchroniczną, ale spowodowałoby to uszkodzenie interfejsu API, a wszyscy wywołujący na stosie również musieliby zostać zaktualizowani (a mogą to być nawet różne aplikacje od różnych dostawców). Lub będziesz musiał dodać wersję asynchroniczną wraz z wersją synchronizowaną, ale nie robi to dużej różnicy - użycie wersji synchronizowanej blokowałoby, a zatem jest mało akceptowalne.
Byłoby wspaniale, gdyby można było uczynić istniejącą metodę synchronizacji asynchroniczną bez zmiany interfejsu API. Ale w rzeczywistości nie mamy takiej opcji, jak sądzę, a używanie wersji asynchronicznej, nawet jeśli nie jest obecnie potrzebna, jest jedynym sposobem na zagwarantowanie, że nigdy nie napotkasz problemów ze zgodnością w przyszłości.
źródło