Nie znalazłem na ten temat wielu zasobów: zastanawiałem się, czy to możliwe / dobry pomysł, aby móc pisać kod asynchroniczny w sposób synchroniczny.
Na przykład, oto kod JavaScript, który pobiera liczbę użytkowników przechowywanych w bazie danych (operacja asynchroniczna):
getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });
Byłoby miło móc napisać coś takiego:
const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);
W ten sposób kompilator automatycznie poczeka na odpowiedź, a następnie uruchomi się console.log
. Zawsze będzie czekać na zakończenie operacji asynchronicznych, zanim wyniki będą musiały być użyte gdziekolwiek indziej. Wykorzystalibyśmy znacznie mniej obietnic wywołania zwrotnego, asynchronizacji / oczekiwania lub czegokolwiek, i nigdy nie musielibyśmy się martwić, czy wynik operacji jest dostępny natychmiast, czy nie.
Błędy nadal byłyby możliwe do zarządzania (czy nbOfUsers
otrzymano liczbę całkowitą lub błąd?) Przy użyciu try / catch lub czegoś takiego jak opcjonalne, jak w języku Swift .
Czy to możliwe? To może być okropny pomysł / utopia ... Nie wiem.
await
saTask<T>
, aby przekształcić goT
async
/await
zamiast, dzięki czemu części asynchroniczne wykonania są jawne.Odpowiedzi:
Async / oczekuj to dokładnie to zautomatyzowane zarządzanie, które proponujesz, aczkolwiek z dwoma dodatkowymi słowami kluczowymi. Dlaczego są ważne? Oprócz kompatybilności wstecznej?
Bez wyraźnych punktów, w których można zawiesić i wznowić korupcję, potrzebowalibyśmy systemu typów do wykrycia, gdzie należy oczekiwać oczekiwanej wartości. Wiele języków programowania nie ma takiego systemu typów.
Wyrażając oczekiwanie na wartość, możemy również przekazywać oczekiwane wartości jako obiekty pierwszej klasy: obietnice. Może to być bardzo przydatne podczas pisania kodu wyższego rzędu.
Kod asynchroniczny ma bardzo głębokie skutki dla modelu wykonania języka, podobnie jak brak lub obecność wyjątków w tym języku. W szczególności na funkcję asynchroniczną mogą oczekiwać tylko funkcje asynchroniczne. Wpływa to na wszystkie funkcje wywoływania! Ale co, jeśli zmienimy funkcję z niesynchronicznej na asynchroniczną na końcu tego łańcucha zależności? Byłaby to zmiana niezgodna wstecz… chyba że wszystkie funkcje są asynchroniczne, a każde wywołanie funkcji jest domyślnie oczekiwane.
Jest to wysoce niepożądane, ponieważ ma bardzo zły wpływ na wydajność. Nie będziesz w stanie po prostu zwrócić tanich wartości. Każde wywołanie funkcji stałoby się znacznie droższe.
Asynchronizacja jest świetna, ale jakaś domniemana asynchronizacja nie działa w rzeczywistości.
Języki czysto funkcjonalne, takie jak Haskell, mają nieco luk zastępczy, ponieważ kolejność wykonywania jest w dużej mierze nieokreślona i niemożliwa do zaobserwowania. Lub sformułowane inaczej: każda konkretna kolejność operacji musi być wyraźnie zakodowana. Może to być dość kłopotliwe w przypadku programów z prawdziwego świata, zwłaszcza tych, które są bardzo obciążone We / Wy, dla których kod asynchroniczny jest bardzo dobrze dopasowany.
źródło
someValue ifItIsAFuture [self| self messageIWantToSend]
dlatego, że integracja z ogólnym kodem jest trudna.par
prawie wszędzie w czysty kod Haskell i uzyskać paralellizm za darmo.To, czego brakuje, to cel operacji asynchronicznych: pozwalają wykorzystać czas oczekiwania!
Jeśli zamienisz operację asynchroniczną, taką jak żądanie jakiegoś zasobu z serwera, na operację synchroniczną, domyślnie i natychmiast czekając na odpowiedź, Twój wątek nie będzie mógł zrobić nic innego z czasem oczekiwania . Jeśli serwer potrzebuje 10 milisekund na odpowiedź, marnuje się około 30 milionów cykli procesora. Opóźnienie odpowiedzi staje się czasem wykonania żądania.
Jedynym powodem, dla którego programiści wymyślili operacje asynchroniczne, jest ukrywanie opóźnień z natury długotrwałych zadań za innymi przydatnymi obliczeniami . Jeśli możesz wypełnić czas oczekiwania użyteczną pracą, zaoszczędzisz czas procesora. Jeśli nie możesz, cóż, nic nie jest stracone przez asynchronizację operacji.
Dlatego zalecam uwzględnienie operacji asynchronicznych udostępnianych przez języki. Są tam, aby zaoszczędzić czas.
źródło
Niektórzy.
Nie są jeszcze głównym nurtem (jeszcze), ponieważ asynchronizacja jest stosunkowo nową funkcją, którą dopiero teraz poczuliśmy, jeśli jest to dobra funkcja lub jak przedstawić ją programistom w sposób przyjazny / użyteczny / ekspresyjny / itp. Istniejące funkcje asynchroniczne są w dużej mierze przykręcone do istniejących języków, które wymagają nieco innego podejścia projektowego.
To powiedziawszy, nie jest to dobry pomysł, aby robić to wszędzie. Częstym niepowodzeniem jest wykonywanie wywołań asynchronicznych w pętli, skutecznie szeregując ich wykonanie. Utajnienie wywołań asynchronicznych może ukryć tego rodzaju błąd. Ponadto, jeśli popierasz ukryty przymus ze strony
Task<T>
(lub odpowiednika twojego języka)T
, może to nieco zwiększyć złożoność / koszty w sprawdzaniu typów i zgłaszaniu błędów, gdy nie jest jasne, które z nich naprawdę chciał programista.Ale to nie są problemy nie do pokonania. Jeśli chciałbyś wesprzeć to zachowanie, prawie na pewno mógłbyś, choć byłyby to kompromisy.
źródło
Są języki, które to robią. Ale w rzeczywistości nie ma takiej potrzeby, ponieważ można to łatwo osiągnąć za pomocą istniejących funkcji językowych.
Tak długo, jak masz jakiś sposób na wyrażenie asynchronii, możesz implementować Futures lub Obietnice wyłącznie jako funkcję biblioteki, nie potrzebujesz żadnych specjalnych funkcji językowych. I dopóki masz trochę wyrażania przezroczystych serwerów proxy , możesz połączyć te dwie funkcje razem i masz przezroczyste kontrakty futures .
Na przykład w Smalltalk i jego potomkach obiekt może zmienić swoją tożsamość, może dosłownie „stać się” innym obiektem (i tak naprawdę nazywana jest metoda, która to robi
Object>>become:
).Wyobraź sobie długotrwałe obliczenia, które zwracają a
Future<Int>
. MaFuture<Int>
to wszystkie te same metodyInt
, z wyjątkiem różnych implementacji.Future<Int>
„s+
sposób nie dodać kolejny numer i zwraca wynik, zwraca nowyFuture<Int>
która owija obliczeń. I tak dalej i tak dalej. Metody, które nie mogą sensownie być realizowane poprzez zwrotFuture<Int>
, będzie zamiast automatycznieawait
rezultat, a następnie zadzwonićself become: result.
, który sprawi, że obiekt aktualnie wykonywanego (self
, tznFuture<Int>
) dosłownie stać sięresult
przedmiotem, czyli od chwili odwołania do obiektu, który kiedyś byłFuture<Int>
to terazInt
wszędzie, całkowicie przejrzysty dla klienta.Nie są potrzebne specjalne funkcje językowe związane z asynchronią.
źródło
Future<T>
i drugieT
mają wspólny interfejs, a ja korzystam z funkcji z tego interfejsu. Czy powinien tobecome
wynikać, a następnie skorzystać z funkcjonalności, czy nie? Mam na myśli takie rzeczy, jak operator równości lub reprezentacja debugowania ciągów.a + b
, obie liczby całkowite, bez względu na to, czy aib są dostępne natychmiast, czy później, piszemya + b
(umożliwiając toInt + Future<Int>
)Future<T>
iT
ponieważ z twojego punktu widzenia nie maFuture<T>
, tylkoT
. Istnieje oczywiście wiele wyzwań inżynieryjnych dotyczących sposobu uczynienia tego wydajnym, które operacje powinny blokować, a nie blokować itp., Ale to naprawdę jest niezależne od tego, czy robisz to jako funkcję języka czy biblioteki. Przejrzystość była wymogiem określonym przez PO w pytaniu, nie będę twierdził, że jest to trudne i może nie mieć sensu.Robią (cóż, większość z nich). Funkcja, której szukasz, nosi nazwę wątków .
Wątki mają jednak własne problemy:
Ponieważ kod można zawiesić w dowolnym momencie , nigdy nie można zakładać, że rzeczy nie zmienią się „same”. Podczas programowania z wątkami tracisz dużo czasu na myślenie o tym, jak twój program powinien radzić sobie ze zmianami.
Wyobraź sobie, że serwer gry przetwarza atak gracza na innego gracza. Coś takiego:
Trzy miesiące później gracz odkrywa, że zabijając się i wylogowując się dokładnie podczas
attacker.addInventoryItems
biegu, a następnievictim.removeInventoryItems
zawiedzie, może zatrzymać swoje przedmioty, a atakujący również otrzyma kopię swoich przedmiotów. Robi to kilka razy, tworząc milion ton złota z powietrza i niszcząc ekonomię gry.Alternatywnie, atakujący może się wylogować, gdy gra wysyła wiadomość do ofiary, a on nie otrzyma etykiety „mordercy” nad głową, więc jego następna ofiara nie ucieknie od niego.
Ponieważ kod można zawiesić w dowolnym momencie , podczas manipulacji strukturami danych należy wszędzie używać blokad. Podałem powyższy przykład, który ma oczywiste konsekwencje w grze, ale może być bardziej subtelny. Rozważ dodanie elementu na początku połączonej listy:
Nie stanowi to problemu, jeśli powiesz, że wątki można zawiesić tylko wtedy, gdy wykonują operacje we / wy, i nie w żadnym momencie. Ale jestem pewien, że możesz sobie wyobrazić sytuację, w której występuje operacja We / Wy - na przykład rejestrowanie:
Ponieważ kod można zawiesić w dowolnym momencie , potencjalnie może być wiele stanów do zapisania. System radzi sobie z tym, nadając każdemu wątkowi zupełnie osobny stos. Ale stos jest dość duży, więc nie możesz mieć więcej niż około 2000 wątków w 32-bitowym programie. Możesz też zmniejszyć rozmiar stosu, ryzykując, że będzie on zbyt mały.
źródło
Znaleźć tu wiele odpowiedzi, które wprowadzają w błąd, ponieważ chociaż pytanie dotyczyło dosłownie programowania asynchronicznego i nieblokującego We / Wy, nie sądzę, abyśmy mogli omówić jedną bez omawiania drugiej w tym konkretnym przypadku.
Podczas gdy programowanie asynchroniczne jest z natury asynchroniczne, racją bytu programowania asynchronicznego jest przede wszystkim unikanie blokowania wątków jądra. Node.js używa asynchroniczności poprzez wywołania zwrotne lub
Promise
s, aby umożliwić wywoływanie operacji blokujących z pętli zdarzeń, a Netty w Javie używa asynchroniczności poprzez wywołania zwrotne lubCompletableFuture
s, aby zrobić coś podobnego.Kod nieblokujący nie wymaga jednak asynchroniczności . To zależy od tego, ile Twój język programowania i środowisko wykonawcze jest w stanie zrobić dla Ciebie.
Go, Erlang i Haskell / GHC poradzą sobie z tym za Ciebie. Możesz napisać coś podobnego
var response = http.get('example.com/test')
i zwolnić wątek jądra za kulisami, czekając na odpowiedź. Odbywa się to za pomocą goroutyn, procesów Erlanga lub porzucaniaforkIO
wątków jądra za kulisami podczas blokowania, pozwalając mu robić inne rzeczy w oczekiwaniu na odpowiedź.To prawda, że język tak naprawdę nie jest w stanie poradzić sobie z asynchronicznością, ale niektóre abstrakcje pozwalają pójść dalej niż inne, np. Nielimitowane kontynuacje lub asymetryczne coroutines. Jednak podstawowa przyczyna kodu asynchronicznego, blokowanie wywołań systemowych, absolutnie może zostać oderwana od programisty.
Node.js i Java obsługują asynchroniczny kod nieblokujący , natomiast Go i Erlang obsługują synchroniczny kod nieblokujący . Oba są poprawnymi podejściami z różnymi kompromisami.
Moim raczej subiektywnym argumentem jest to, że ci, którzy argumentują przeciwko środowisku wykonawczemu zarządzającym nieblokowaniem w imieniu dewelopera, są jak ci, którzy argumentowali przeciwko usuwaniu śmieci we wczesnych latach dziewięćdziesiątych. Tak, pociąga to za sobą koszty (w tym przypadku przede wszystkim więcej pamięci), ale ułatwia programowanie i debugowanie oraz sprawia, że podstawy kodu są bardziej niezawodne.
Osobiście uważam, że asynchroniczny nieblokujący kod powinien być zarezerwowany dla programowania systemów w przyszłości, a bardziej nowoczesne stosy technologii powinny migrować do synchronicznych nieblokujących środowisk uruchomieniowych do programowania aplikacji.
źródło
waitpid(..., WNOHANG)
zawodzi, jeśli musiałoby się blokować. Czy też „synchroniczny” oznacza tutaj „nie ma widocznych dla programisty wywołań zwrotnych / automatów stanów / pętli zdarzeń”? Ale dla twojego przykładu Go nadal muszę wyraźnie oczekiwać wyniku od goroutine czytając z kanału, nie? Jak to jest mniej asynchroniczne niż asynchroniczne / oczekujące w JS / C # / Python?Jeśli dobrze cię czytam, pytasz o model programowania synchronicznego, ale o wysoką wydajność. Jeśli jest to poprawne, to jest już dla nas dostępne w postaci zielonych wątków lub procesów np. Erlang lub Haskell. Tak, to doskonały pomysł, ale modernizacja istniejących języków nie zawsze jest tak płynna, jak byś chciał.
źródło
Doceniam to pytanie i uważam, że większość odpowiedzi jest jedynie obroną status quo. W spektrum języków od niskiego do wysokiego poziomu utknęliśmy w rutynie od jakiegoś czasu. Następny wyższy poziom będzie wyraźnie językiem mniej skoncentrowanym na składni (potrzeba wyraźnych słów kluczowych, takich jak oczekiwanie i asynchronizacja), a wiele więcej na temat intencji. (Oczywiste uznanie dla Charlesa Simonyi, ale z myślą o 2019 roku i przyszłości.)
Jeśli powiedziałem programatorowi, napisz kod, który po prostu pobiera wartość z bazy danych, możesz bezpiecznie założyć, że mam na myśli „i BTW, nie zawieszaj interfejsu użytkownika” i „nie wprowadzaj innych uwag, które maskują trudne do znalezienia błędów „. Programiści przyszłości, z następną generacją języków i narzędzi, z pewnością będą mogli pisać kod, który po prostu pobiera wartość w jednym wierszu kodu i stamtąd.
Językiem na najwyższym poziomie będzie mówienie po angielsku i poleganie na kompetencjach osoby wykonującej zadanie, aby wiedzieć, co naprawdę chcesz zrobić. (Pomyśl o komputerze w Star Trek lub pytaniu o Alexę.) Jesteśmy daleko od tego, ale zbliżamy się i oczekuję, że język / kompilator może bardziej generować solidny, intencjonalny kod, nie sięgając nawet potrzebuje AI.
Z jednej strony istnieją nowsze języki wizualne, takie jak Scratch, które to robią i nie są zaprzęgnięte wszystkimi technicznymi składniami. Na pewno dzieje się wiele zakulisowych prac, więc programista nie musi się tym martwić. To powiedziawszy, nie piszę oprogramowania klasy biznesowej w Scratch, więc podobnie jak ty oczekuję, że nadszedł czas, aby dojrzałe języki programowania automatycznie zarządzały problemem synchronicznym / asynchronicznym.
źródło
Opisany problem jest dwojaki.
Jest kilka sposobów na osiągnięcie tego, ale w zasadzie sprowadzają się do
foo(4, 7, bar, quux)
.W przypadku (1) skupiam się na rozwidlaniu i uruchamianiu wielu procesów, spawnowaniu wielu wątków jądra i implementacjach zielonego wątku, które planują wątki poziomu języka wykonawczego na wątki jądra. Z punktu widzenia problemu są one takie same. Na tym świecie żadna funkcja nigdy się nie poddaje ani nie traci kontroli z perspektywy swojego wątku . Sam wątek czasami nie ma kontroli i czasem nie działa, ale nie rezygnujesz z kontroli nad własnym wątkiem na tym świecie. System pasujący do tego modelu może, ale nie musi, odradzać nowe wątki lub łączyć się z istniejącymi wątkami. System pasujący do tego modelu może, ale nie musi, mieć zdolność duplikowania wątku takiego jak Unix
fork
.(2) jest interesujące. Aby tego dokonać, musimy porozmawiać o formularzach wprowadzających i eliminujących.
Pokażę, dlaczego
await
nie można dodać niejawnego do języka takiego jak Javascript w sposób zgodny z poprzednimi wersjami. Podstawową ideą jest to, że poprzez obnażanie obietnic użytkownikowi i rozróżnienie kontekstów synchronicznych i asynchronicznych, Javascript wyciekł ze szczegółami implementacji, które uniemożliwiają równomierne obsługiwanie funkcji synchronicznych i asynchronicznych. Istnieje również fakt, że nie możnaawait
obiecać poza ciałem funkcji asynchronicznej. Te opcje projektowania są niezgodne z „powodowaniem, że asynchroniczność jest niewidoczna dla dzwoniącego”.Możesz wprowadzić funkcję synchroniczną za pomocą lambda i wyeliminować ją za pomocą wywołania funkcji.
Wprowadzenie do funkcji synchronicznej:
Eliminacja funkcji synchronicznej:
Można to porównać z wprowadzaniem i eliminowaniem funkcji asynchronicznych.
Wprowadzenie do funkcji asynchronicznej
Eliminacja funkcji asynchronicznej (uwaga: obowiązuje tylko wewnątrz
async
funkcji)Podstawowym problemem jest to, że funkcja asynchroniczna jest również funkcją synchroniczną, która tworzy obiekt obietnicy .
Oto przykład wywoływania funkcji asynchronicznej synchronicznie w replie node.js.
Możesz hipotetycznie mieć język, nawet dynamiczny, w którym różnica między wywołaniami funkcji asynchronicznej i synchronicznej nie jest widoczna w miejscu wywołania i prawdopodobnie nie jest widoczna w miejscu definicji.
Biorąc taki język i obniżając go do Javascript jest możliwe, po prostu trzeba by skutecznie sprawić, by wszystkie funkcje były asynchroniczne.
źródło
Dzięki goroutynom języka Go i czasowi działania języka Go możesz pisać cały kod tak, jakby był on synchronizowany. Jeśli operacja zablokuje się w jednym goroutine, wykonywanie będzie kontynuowane w innych goroutine. A dzięki kanałom możesz łatwo komunikować się między goroutynami. Jest to często łatwiejsze niż wywołania zwrotne, takie jak w JavaScript lub async / czekają w innych językach. Zobacz https://tour.golang.org/concurrency/1 dla niektórych przykładów i wyjaśnień.
Co więcej, nie mam z tym osobistego doświadczenia, ale słyszę, że Erlang ma podobne możliwości.
Tak, istnieją języki programowania, takie jak Go i Erlang, które rozwiązują problem synchroniczności / asynchroniczności, ale niestety nie są jeszcze zbyt popularne. W miarę wzrostu popularności tych języków być może zapewnione przez nich udogodnienia zostaną wdrożone także w innych językach.
źródło
go ...
, więc wygląda podobnie jakawait ...
nie?go
. I prawie każde wywołanie, które może blokować, jest wykonywane asynchronicznie przez środowisko wykonawcze, które w międzyczasie przełącza się na inną goroutine (wielozadaniowość kooperacyjna). Czekasz na wiadomość.await
jest odczyt z kanału<- ch
.Istnieje bardzo ważny aspekt, który nie został jeszcze podniesiony: ponowne powołanie. Jeśli masz inny kod (np .: pętla zdarzeń), który działa podczas wywołania asynchronicznego (a jeśli nie, to dlaczego w ogóle potrzebujesz asynchronizacji?), Kod może wpłynąć na stan programu. Nie można ukryć wywołań asynchronicznych przed wywołującym, ponieważ wywołujący może zależeć od części stanu programu, aby pozostać niezmieniony przez czas trwania wywołania funkcji. Przykład:
Jeśli
bar()
jest funkcją asynchroniczną, zmiana może być możliwaobj.x
podczas jej wykonywania. Byłoby to raczej nieoczekiwane bez żadnej wskazówki, że pasek jest asynchroniczny i ten efekt jest możliwy. Jedyną alternatywą byłoby podejrzenie, że każda możliwa funkcja / metoda jest asynchroniczna i ponownie pobiera i ponownie sprawdza stan nielokalny po każdym wywołaniu funkcji. Jest to podatne na subtelne błędy i może nawet nie być możliwe, jeśli niektóre nielokalne stany są pobierane za pomocą funkcji. Z tego powodu programista musi wiedzieć, które funkcje mogą potencjalnie zmienić stan programu w nieoczekiwany sposób:Teraz jest wyraźnie widoczne, że
bar()
jest to funkcja asynchroniczna, a poprawnym sposobem jej obsługi jest ponowne sprawdzenie oczekiwanej wartościobj.x
później i zajęcie się wszelkimi zmianami, które mogły wystąpić.Jak już wspomniano w innych odpowiedziach, wyłącznie funkcjonalne języki, takie jak Haskell, mogą całkowicie uniknąć tego efektu, całkowicie eliminując potrzebę jakiegokolwiek wspólnego / globalnego stanu. Nie mam dużego doświadczenia z językami funkcjonalnymi, więc prawdopodobnie jestem stronniczy, ale nie sądzę, że brak stanu globalnego jest zaletą przy pisaniu większych aplikacji.
źródło
W przypadku Javascript, którego użyłeś w swoim pytaniu, należy pamiętać o: Javascript jest jednowątkowy, a kolejność wykonywania jest gwarantowana, dopóki nie ma wywołań asynchronicznych.
Więc jeśli masz taką sekwencję:
Masz gwarancję, że nic więcej nie zostanie w międzyczasie wykonane. Nie potrzeba zamków ani niczego podobnego.
Jeśli jednak
getNbOfUsers
jest asynchroniczny, to:oznacza, że podczas
getNbOfUsers
uruchomień wykonanie daje zysk, a pomiędzy nimi może działać inny kod. To z kolei może wymagać pewnego zablokowania, w zależności od tego, co robisz.Warto więc wiedzieć, kiedy połączenie jest asynchroniczne, a kiedy nie, ponieważ w niektórych sytuacjach konieczne będzie podjęcie dodatkowych środków ostrożności, których nie byłoby konieczne, gdyby połączenie było synchroniczne.
źródło
getNbOfUsers()
zwrócił Obietnicę. Ale o to właśnie chodzi w moim pytaniu: dlaczego musimy jawnie napisać to jako asynchroniczne, kompilator może to wykryć i obsługiwać automatycznie w inny sposób.Jest to dostępne w C ++
std::async
od wersji C ++ 11.A w C ++ 20 można używać coroutines:
źródło
await
(lubco_await
w tym przypadku) słowa kluczowego?