To pytanie dotyczy języka C #, ale spodziewam się, że obejmie inne języki, takie jak Java lub TypeScript.
Microsoft zaleca najlepsze praktyki korzystania z wywołań asynchronicznych w .NET. Spośród tych rekomendacji wybierzmy dwa:
- zmień podpis metod asynchronicznych, tak aby zwracały Zadanie lub Zadanie <> (w TypeScript byłoby to Obietnicą <>)
- zmień nazwy metod asynchronicznych na końcówkę xxxAsync ()
Teraz, gdy zamieniasz synchroniczny komponent niskiego poziomu na asynchroniczny, wpływa to na cały stos aplikacji. Ponieważ async / await ma pozytywny wpływ tylko wtedy, gdy jest używany „do samego końca”, oznacza to, że podpis i nazwy metod każdej warstwy w aplikacji muszą zostać zmienione.
Dobra architektura często wymaga umieszczania abstrakcji między poszczególnymi warstwami, tak że zamiana komponentów niskiego poziomu na inne jest niewidoczna dla komponentów wyższego poziomu. W języku C # abstrakcje mają formę interfejsów. Jeśli wprowadzimy nowy komponent asynchroniczny niskiego poziomu, każdy interfejs w stosie wywołań musi zostać zmodyfikowany lub zastąpiony nowym interfejsem. Sposób rozwiązania problemu (asynchronizacja lub synchronizacja) w klasie implementującej nie jest już ukryty (abstrakcyjny) dla dzwoniących. Dzwoniący muszą wiedzieć, czy jest to synchronizacja czy asynchronizacja.
Czy asynchronizacja / oczekiwanie na najlepsze praktyki nie są sprzeczne z zasadami „dobrej architektury”?
Czy to oznacza, że każdy interfejs (powiedzmy IEnumerable, IDataAccessLayer) potrzebuje swojego asynchronicznego odpowiednika (IAsyncEnumerable, IAsyncDataAccessLayer), aby można je było zamienić na stosie podczas przełączania na zależności asynchroniczne?
Jeśli posuniemy problem nieco dalej, czy nie byłoby łatwiej założyć, że każda metoda jest asynchroniczna (aby zwrócić Zadanie <> lub Obietnica <>), oraz metody synchronizacji wywołań asynchronicznych, gdy tak naprawdę nie są asynchronizować? Czy tego należy się spodziewać w przyszłych językach programowania?
źródło
CancellationToken
, a te, które to robią, mogą chcieć podać wartość domyślną). Usunięcie istniejących metod synchronizacji (i proaktywne złamanie całego kodu) jest oczywistym początkiem.Odpowiedzi:
Jakiego koloru jest twoja funkcja?
Możesz być zainteresowany tym, jaki kolor ma twoja funkcja Boba Nystrom 1 .
W tym artykule opisuje fikcyjny język, w którym:
Choć jest fikcyjny, dzieje się to dość regularnie w językach programowania:
this
.Jak zauważyłeś, z powodu tych reguł czerwone funkcje mają tendencję do rozprzestrzeniania się wokół podstawy kodu. Wstawiasz jeden i stopniowo kolonizuje on całą bazę kodu.
1 Bob Nystrom, oprócz blogowania, jest również częścią zespołu Dart i napisał tę małą serię Crafting Interpreters; wysoce zalecane dla każdego miłośnika języka programowania / kompilatora.
2 Niezupełnie prawda, ponieważ możesz wywołać funkcję asynchroniczną i blokować ją aż do jej powrotu, ale ...
Ograniczenia językowe
Zasadniczo jest to ograniczenie dotyczące języka / czasu wykonywania.
Język z wątkami M: N, na przykład Erlang i Go, nie ma
async
funkcji: każda funkcja jest potencjalnie asynchroniczna, a jej „włókno” zostanie po prostu zawieszone, zamienione i zamienione z powrotem, gdy będzie ponownie gotowe.C # poszedł z modelem wątków 1: 1 i dlatego postanowił odkryć synchroniczność w języku, aby uniknąć przypadkowego blokowania wątków.
Wobec ograniczeń językowych wytyczne kodowania muszą zostać dostosowane.
źródło
async
); w rzeczywistości był to dość powszechny wzorzec budowania responsywnego interfejsu użytkownika. Czy to było tak piękne jak Erlang? Nie. Ale to wciąż daleko od C :)Masz rację, istnieje tutaj sprzeczność, ale nie jest to „najlepsza praktyka”, która jest zła. Wynika to z faktu, że funkcja asynchroniczna zasadniczo różni się od funkcji synchronicznej. Zamiast czekać na wynik z jego zależności (zwykle niektóre operacje we / wy) tworzy zadanie do obsługi przez główną pętlę zdarzeń. To nie jest różnica, którą można dobrze ukryć pod abstrakcją.
źródło
async
metod w języku C #, aby zapewnić kontrakty synchroniczne, jeśli chcesz. Jedyna różnica polega na tym, że Go jest domyślnie asynchroniczny, podczas gdy C # jest domyślnie synchroniczny.async
daje ci drugi model programowania -async
jest abstrakcją (to, co faktycznie robi, zależy od środowiska wykonawczego, harmonogramu zadań, kontekstu synchronizacji, implementacji oczekiwania).Metoda asynchroniczna zachowuje się inaczej niż metoda synchroniczna, o czym jestem pewien. W czasie wykonywania konwersja wywołania asynchronicznego na synchroniczne jest banalna, ale nie można tego powiedzieć przeciwnie. Tak więc logika staje się następująca: dlaczego nie stworzymy metod asynchronicznych dla każdej metody, która może jej wymagać i nie pozwolimy, by osoba wywołująca „przekonwertowała” w razie potrzeby metodę synchroniczną?
W pewnym sensie przypomina to metodę, która rzuca wyjątki i drugą, która jest „bezpieczna” i nie rzuca się nawet w przypadku błędu. W którym momencie program kodujący przesadza z dostarczaniem tych metod, które w przeciwnym razie mogłyby zostać przekonwertowane na inne?
Są w tym dwie szkoły myślenia: jedna polega na stworzeniu wielu metod, każda z nich wywołuje inną, być może prywatną metodę, umożliwiającą zapewnienie opcjonalnych parametrów lub drobnych zmian w zachowaniu, takich jak zachowanie asynchroniczne. Drugim jest zminimalizowanie metod interfejsu do niezbędnego minimum, pozostawiając dzwoniącemu samodzielne wykonanie niezbędnych modyfikacji.
Jeśli należysz do pierwszej szkoły, logika jest poświęcona klasie na połączenia synchroniczne i asynchroniczne, aby uniknąć podwojenia każdego połączenia. Microsoft ma tendencję do faworyzowania tej szkoły myślenia i zgodnie z konwencją, aby zachować spójność ze stylem preferowanym przez Microsoft, ty również musiałbyś mieć wersję Async, w taki sam sposób, w jaki interfejsy prawie zawsze zaczynają się od „ja”. Podkreślę, że nie jest to złe samo w sobie, ponieważ lepiej jest zachować spójny styl w projekcie niż robić to „we właściwy sposób” i radykalnie zmienić styl rozwoju, który dodajesz do projektu.
To powiedziawszy, preferuję drugą szkołę, która polega na minimalizacji metod interfejsu. Jeśli myślę, że metodę można wywołać w sposób asynchroniczny, metoda ta jest dla mnie asynchroniczna. Dzwoniący może zdecydować, czy czekać przed zakończeniem tego zadania, czy nie. Jeśli ten interfejs jest interfejsem do biblioteki, bardziej uzasadnione jest zrobienie tego w ten sposób, aby zminimalizować liczbę metod, które trzeba by wycofać lub dostosować. Jeśli interfejs jest do użytku wewnętrznego w moim projekcie, dodam metodę dla każdego potrzebnego wywołania w całym projekcie dla podanych parametrów i żadnych „dodatkowych” metod, a nawet wtedy, tylko jeśli zachowanie metody nie jest jeszcze uwzględnione za pomocą istniejącej metody.
Jednak, jak wiele rzeczy w tej dziedzinie, jest to w dużej mierze subiektywne. Oba podejścia mają swoje zalety i wady. Microsoft rozpoczął także konwencję dodawania liter oznaczających typ na początku nazwy zmiennej, a „m_” oznacza, że jest członkiem, co prowadzi do nazw zmiennych takich jak
m_pUser
. Chodzi mi o to, że nawet Microsoft nie jest nieomylny i może popełniać błędy.To powiedziawszy, jeśli twój projekt jest zgodny z konwencją Async, radzę ci go uszanować i kontynuować styl. I tylko po otrzymaniu własnego projektu możesz napisać go w najlepszy dla siebie sposób.
źródło
.Wait()
metody itp. Może powodować negatywne konsekwencje, aw js, o ile wiem, nie jest to w ogóle możliwe.Promise.resolve(x)
a następnie dodasz wywołania zwrotne, wywołania te nie zostaną wykonane natychmiast.Wyobraźmy sobie, że istnieje sposób na wywołanie funkcji w sposób asynchroniczny bez zmiany ich podpisu.
To byłoby naprawdę fajne i nikt nie zaleciłby zmiany ich nazw.
Ale rzeczywiste funkcje asynchroniczne, nie tylko te, które oczekują na inną funkcję asynchroniczną, ale najniższy poziom mają dla nich pewną strukturę charakterystyczną dla ich asynchronicznej natury. na przykład
Są to te dwie informacje, których potrzebuje kod wywołujący, aby uruchomić kod w sposób asynchroniczny, który jest podobny
Task
iasync
hermetyzowany.Jest więcej niż jeden sposób wykonywania funkcji asynchronicznych,
async Task
jest tylko jeden wzorzec wbudowany w kompilator za pośrednictwem typów zwracanych, dzięki czemu nie trzeba ręcznie łączyć zdarzeńźródło
Zajmę się głównym punktem w sposób mniej C # ness i bardziej ogólny:
Powiedziałbym, że zależy to tylko od wyboru dokonanego przy projektowaniu interfejsu API i tego, co pozwolisz użytkownikowi.
Jeśli chcesz, aby jedna funkcja interfejsu API była tylko asynchroniczna, nie ma większego zainteresowania przestrzeganiem konwencji nazewnictwa. Po prostu zawsze zwracaj Zadanie <> / Obietnica <> / Przyszłość <> / ... jako typ zwrotu, jest to samo dokumentowanie. Jeśli chce zsynchronizować odpowiedź, nadal będzie w stanie to zrobić, czekając, ale jeśli zawsze to zrobi, zrobi to trochę bzdury.
Jeśli jednak synchronizujesz tylko interfejs API, oznacza to, że jeśli użytkownik chce, aby był on asynchroniczny, będzie musiał sam zarządzać częścią asynchroniczną.
Może to spowodować sporo dodatkowej pracy, ale może również dać użytkownikowi większą kontrolę nad tym, ile dozwolonych jest jednoczesnych połączeń, limit czasu, retrys i tak dalej.
W dużym systemie z ogromnym interfejsem API wdrożenie większości z nich w celu domyślnej synchronizacji może być łatwiejsze i bardziej wydajne niż niezależne zarządzanie każdą częścią interfejsu API, zwłaszcza jeśli współużytkują zasoby (system plików, procesor, baza danych ...).
W rzeczywistości w przypadku najbardziej skomplikowanych części można idealnie mieć dwie implementacje tej samej części interfejsu API, jedną synchroniczną wykonującą przydatne funkcje, drugą asynchroniczną polegającą na synchronicznej obsługującą różne operacje i zarządzającą tylko współbieżnością, obciążeniami, limitami czasu i ponownymi próbami .
Może ktoś inny może podzielić się z tym swoim doświadczeniem, ponieważ brakuje mi doświadczenia z takimi systemami.
źródło