Dlaczego języki programowania nie zarządzają automatycznie problemem synchronicznym / asynchronicznym?


27

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 nbOfUsersotrzymano 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.


58
Naprawdę nie rozumiem twojego pytania. Jeśli „zawsze czekasz na operację asynchroniczną”, nie jest to operacja asynchroniczna, lecz operacja synchroniczna. Możesz wyjaśnić? Może podaj specyfikację rodzaju zachowania, którego szukasz? Również „co o tym sądzisz” jest nie na temat inżynierii oprogramowania . Musisz sformułować swoje pytanie w kontekście konkretnego problemu, który ma jedną, jednoznaczną, kanoniczną, obiektywnie poprawną odpowiedź.
Jörg W Mittag

4
@ JörgWMittag sobie wyobrazić hipotetyczny C #, która w domyśle awaitsa Task<T>, aby przekształcić goT
Caleth

6
To, co proponujesz, nie jest wykonalne. Kompilator nie musi decydować, czy chcesz poczekać na wynik, czy może odpalić i zapomnieć. Lub biegnij w tle i czekaj później. Po co się tak ograniczać?
zakręcony

5
Tak, to okropny pomysł. Wystarczy użyć async/ awaitzamiast, dzięki czemu części asynchroniczne wykonania są jawne.
Bergi

5
Kiedy mówisz, że dwie rzeczy dzieją się jednocześnie, mówisz, że to w porządku, że te rzeczy dzieją się w dowolnej kolejności. Jeśli Twój kod nie ma sposobu, aby wyjaśnić, które ponowne zamówienia nie złamią oczekiwań Twojego kodu, nie będzie w stanie zapewnić ich współbieżności.
Rob

Odpowiedzi:


65

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.


2
Nie potrzebujesz systemu typu. Przejrzyste kontrakty futures, np. ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, można łatwo wdrożyć bez systemu tyoe lub obsługi języka. W Smalltalk i jego potomkach obiekt może w przejrzysty sposób zmienić swoją tożsamość, w ECMAScript może w przejrzysty sposób zmienić swój kształt. To wszystko, czego potrzebujesz, aby uczynić Futures przejrzystym, bez potrzeby obsługi języka dla asynchronii.
Jörg W Mittag

6
@ JörgWMittag Rozumiem, co mówisz i jak to może działać, ale przejrzyste kontrakty bez systemu typów utrudniają jednoczesne posiadanie kontraktów pierwszej klasy, prawda? Potrzebowałbym jakiegoś sposobu, aby wybrać, czy chcę wysyłać wiadomości do przyszłości, czy do wartości przyszłości, najlepiej coś lepszego niż someValue ifItIsAFuture [self| self messageIWantToSend]dlatego, że integracja z ogólnym kodem jest trudna.
amon

8
@amon „Mogę napisać kod asynchroniczny, ponieważ obietnice i obietnice są monadami”. Monady nie są tutaj tak naprawdę potrzebne. Gromady są w gruncie rzeczy tylko obietnicami. Ponieważ prawie wszystkie wartości w Haskell są umieszczone w ramkach, prawie wszystkie wartości w Haskell są już obiecane. Dlatego możesz rzucić parprawie wszędzie w czysty kod Haskell i uzyskać paralellizm za darmo.
DarthFennec

2
Async / czekaj przypomina mi kontynuację monady.
les

3
W rzeczywistości zarówno wyjątki, jak i asynchronizacja / oczekiwanie to przypadki efektów algebraicznych .
Alex Reinking

21

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.


myślałem o funkcjonalnym języku, w którym operacje nie blokują, więc nawet jeśli ma on składnię synchroniczną, długo działające obliczenia nie zablokują wątku
Cinn

6
@Cinn Nie znalazłem tego w pytaniu, a przykładem w pytaniu jest Javascript, który nie ma tej funkcji. Jednak generalnie trudno jest kompilatorowi znaleźć znaczące możliwości równoległości, tak jak to opisano: znaczące wykorzystanie takiej funkcji wymagałoby od programisty wyraźnego przemyślenia tego , co naprawili po długim wywołaniu opóźnienia. Jeśli środowisko wykonawcze jest wystarczająco inteligentne, aby uniknąć tego wymogu programisty, środowisko wykonawcze prawdopodobnie pochłonie oszczędności w wydajności, ponieważ musiałoby agresywnie działać równolegle między wywołaniami funkcji.
cmaster

2
Wszystkie komputery czekają z tą samą prędkością.
Bob Jarvis - Przywróć Monikę

2
@BobJarvis Tak. Różnią się jednak ilością pracy, jaką mogliby wykonać w czasie oczekiwania ...
cmaster

13

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.


1
Myślę, że pomysłem może być zawinięcie wszystkiego w funkcje asynchroniczne, zadania synchroniczne po prostu zostałyby natychmiast rozwiązane, a my mamy do czynienia z jednym rodzajem do obsługi (Edycja: @amon wyjaśnił, dlaczego to zły pomysł ...)
Cinn

8
Czy możesz podać kilka przykładów „ Some do ”?
Bergi

2
Programowanie asynchroniczne nie jest w żaden sposób nowe, po prostu w dzisiejszych czasach ludzie mają do czynienia z nim częściej.
Cubic

1
@Cubic - o ile wiem, jest to funkcja językowa. Wcześniej były to (niezręczne) funkcje przestrzeni użytkownika.
Telastyn

12

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>. Ma Future<Int>to wszystkie te same metody Int, z wyjątkiem różnych implementacji. Future<Int>„s +sposób nie dodać kolejny numer i zwraca wynik, zwraca nowy Future<Int>która owija obliczeń. I tak dalej i tak dalej. Metody, które nie mogą sensownie być realizowane poprzez zwrot Future<Int>, będzie zamiast automatycznie awaitrezultat, a następnie zadzwonić self become: result., który sprawi, że obiekt aktualnie wykonywanego ( self, tzn Future<Int>) dosłownie stać się resultprzedmiotem, czyli od chwili odwołania do obiektu, który kiedyś był Future<Int>to teraz Intwszędzie, całkowicie przejrzysty dla klienta.

Nie są potrzebne specjalne funkcje językowe związane z asynchronią.


Ok, ale to ma problemy, jeśli jedno Future<T>i drugie Tmają wspólny interfejs, a ja korzystam z funkcji z tego interfejsu. Czy powinien to becomewynikać, 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.
amon

Rozumiem, że nie dodaje żadnych funkcji, chodzi o to, że mamy różne składnie do pisania obliczeń natychmiast rozwiązujących i obliczeń długotrwałych, a następnie używamy wyników w ten sam sposób do innych celów. Zastanawiałem się, czy moglibyśmy mieć składnię, która w przejrzysty sposób obsługuje oba te elementy, czyniąc ją bardziej czytelną, a więc programista nie musi jej obsługiwać. Jak robienie a + b, obie liczby całkowite, bez względu na to, czy aib są dostępne natychmiast, czy później, piszemy a + b(umożliwiając to Int + Future<Int>)
Cinn

@Cinn: Tak, możesz to zrobić za pomocą Transparent Futures i nie potrzebujesz do tego żadnych specjalnych funkcji językowych. Możesz go zaimplementować przy użyciu już istniejących funkcji, np. Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript i najwyraźniej, jak właśnie czytam, Python.
Jörg W Mittag

3
@amon: Idea Transparent Futures polega na tym, że nie wiesz, że to przyszłość. Z twojego punktu widzenia nie ma wspólnego interfejsu Future<T>i Tponieważ z twojego punktu widzenia nie maFuture<T> , tylko T. 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.
Jörg W Mittag

3
@ Jörg Wydaje się, że byłoby to problematyczne we wszystkich językach oprócz funkcjonalnych, ponieważ nie masz możliwości dowiedzenia się, kiedy kod jest faktycznie wykonywany w tym modelu. To na ogół działa dobrze, powiedzmy Haskell, ale nie widzę, jak to by działało w bardziej proceduralnych językach (a nawet w Haskell, jeśli zależy ci na wydajności, czasami musisz wymusić wykonanie i zrozumieć podstawowy model). Niemniej ciekawy pomysł.
Voo

7

Robią (cóż, większość z nich). Funkcja, której szukasz, nosi nazwę wątków .

Wątki mają jednak własne problemy:

  1. 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:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Trzy miesiące później gracz odkrywa, że ​​zabijając się i wylogowując się dokładnie podczas attacker.addInventoryItemsbiegu, a następnie victim.removeInventoryItemszawiedzie, 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.

  2. 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:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    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:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. 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.


3

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 Promises, aby umożliwić wywoływanie operacji blokujących z pętli zdarzeń, a Netty w Javie używa asynchroniczności poprzez wywołania zwrotne lub CompletableFutures, 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 porzucania forkIOwą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.


1
To była naprawdę interesująca odpowiedź! Ale nie jestem pewien, czy rozumiem twoje rozróżnienie między „synchronicznym” a „asynchronicznym” nieblokującym kodem. Dla mnie synchroniczny nieblokujący kod oznacza, że ​​coś takiego jak funkcja C 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?
amon

1
Używam „asynchronicznych” i „synchronicznych” do omawiania modelu programowania wystawionego na programistę oraz „blokowania” i „nieblokowania” do omawiania blokowania wątku jądra, podczas którego nie może on zrobić nic użytecznego, nawet jeśli istnieją inne obliczenia, które należy wykonać, i istnieje zapasowy procesor logiczny, którego można użyć. Cóż, goroutine może po prostu czekać na wynik, nie blokując głównego wątku, ale inny goroutine może komunikować się z nim przez kanał, jeśli chce. Goroutine nie musi jednak używać kanału bezpośrednio do oczekiwania na odczyt nieblokującego gniazda.
Louis Jackman

Hmm ok, teraz rozumiem twoje rozróżnienie. Podczas gdy bardziej martwię się zarządzaniem przepływem danych i kontrolą między coroutines, bardziej martwi cię to, że nigdy nie blokujesz głównego wątku jądra. Nie jestem pewien, czy Go lub Haskell mają pod tym względem jakąkolwiek przewagę nad C ++ lub Javą, ponieważ one także mogą odpalić wątki w tle, ponieważ wymaga to tylko odrobiny kodu.
amon

@LouisJackman może nieco rozwinąć twoje ostatnie zdanie na temat nieblokowania asynchronicznego dla programowania systemu. Jakie są zalety asynchronicznego podejścia nieblokującego?
sunprophit

@sunprophit Asynchroniczne nieblokowanie to tylko transformacja kompilatora (zwykle asynchronizacja / oczekiwanie), podczas gdy synchroniczne nieblokowanie wymaga obsługi środowiska wykonawczego, takiego jak kombinacja złożonych operacji na stosie, wstawianie punktów wydajności przy wywołaniach funkcji (które mogą kolidować z wstawianiem), śledzenie „ redukcje ”(wymagające maszyny wirtualnej, takiej jak BEAM) itp. Podobnie jak wyrzucanie elementów bezużytecznych, kompromituje mniej złożoności środowiska wykonawczego dla łatwości użycia i niezawodności. Języki systemowe, takie jak C, C ++ i Rust, unikają takich większych funkcji środowiska wykonawczego ze względu na swoje docelowe domeny, więc asynchroniczne nieblokowanie ma sens.
Louis Jackman,

2

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ł.


2

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.


1

Opisany problem jest dwojaki.

  • Program, który piszesz, powinien zachowywać się asynchronicznie jako całość , patrząc z zewnątrz .
  • W miejscu wywołania nie powinno być widoczne, czy wywołanie funkcji potencjalnie poddaje się kontroli, czy nie.

Jest kilka sposobów na osiągnięcie tego, ale w zasadzie sprowadzają się do

  1. posiadanie wielu wątków (na pewnym poziomie abstrakcji)
  2. posiadające wiele rodzajów funkcji na poziomie językowym, z których wszystkie są nazywane w ten sposób 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 awaitnie 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żna awaitobiecać 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:

((x) => {return x + x;})

Eliminacja funkcji synchronicznej:

f(4)

((x) => {return x + x;})(4)

Można to porównać z wprowadzaniem i eliminowaniem funkcji asynchronicznych.

Wprowadzenie do funkcji asynchronicznej

(async (x) => {return x + x;})

Eliminacja funkcji asynchronicznej (uwaga: obowiązuje tylko wewnątrz asyncfunkcji)

await (async (x) => {return x + x;})(4)

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.

> (async (x) => {return x + x;})(4)
Promise { 8 }

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.


1

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.


Prawie nigdy nie używałem języka Go, ale wygląda na to, że wyraźnie deklarujesz go ..., więc wygląda podobnie jak await ...nie?
Cinn

1
@Cinn Właściwie nie. Możesz umieścić dowolne połączenie jako goroutine na jego własnym włóknie / zielonej nici za pomocą 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ść.
Deduplicator

2
Chociaż Goroutine są rodzajem współbieżności, nie umieszczałbym ich w tym samym segmencie, co asynchronizacja / czekanie: nie kooperatywne korporacje, ale automatycznie (i zapobiegawczo!) Zaplanowane zielone wątki. Nie oznacza to jednak, że czekanie jest automatyczne: odpowiednikiem Go awaitjest odczyt z kanału <- ch.
amon

@amon O ile mi wiadomo, programy uruchamiające współpracują z wątkami natywnymi (zwykle wystarczającymi, aby zmaksymalizować prawdziwą równoległość sprzętową) przez środowisko wykonawcze, a te są wstępnie zaplanowane przez system operacyjny.
Deduplicator

OP poprosił „o możliwość zapisu kodu asynchronicznego w sposób synchroniczny”. Jak już wspomniałeś, dzięki goroutines i środowisku uruchomieniowemu możesz dokładnie to zrobić. Nie musisz się martwić o szczegóły wątków, po prostu pisz blokujące odczyty i zapisy, tak jakby kod był zsynchronizowany, a twoje inne goroutine, jeśli w ogóle, będą działały. Nie musisz nawet „czekać” lub czytać z kanału, aby uzyskać tę korzyść. Dlatego uważam, że Go to język programowania, który najbardziej odpowiada pragnieniom PO.

1

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:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Jeśli bar()jest funkcją asynchroniczną, zmiana może być możliwa obj.xpodczas 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:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Teraz jest wyraźnie widoczne, że bar()jest to funkcja asynchroniczna, a poprawnym sposobem jej obsługi jest ponowne sprawdzenie oczekiwanej wartości obj.xpóź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.


0

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ę:

const nbOfUsers = getNbOfUsers();

Masz gwarancję, że nic więcej nie zostanie w międzyczasie wykonane. Nie potrzeba zamków ani niczego podobnego.

Jeśli jednak getNbOfUsersjest asynchroniczny, to:

const nbOfUsers = await getNbOfUsers();

oznacza, że ​​podczas getNbOfUsersuruchomień 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.


Masz rację, mój drugi kod w pytaniu jest nieprawidłowy, jakby 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.
Cinn

@Cinn nie o to mi chodzi. Chodzi mi o to, że przepływ wykonania może dostać się do innych części kodu podczas wykonywania wywołania asynchronicznego, podczas gdy wywołanie synchroniczne nie jest możliwe. To tak, jakby mieć uruchomionych wiele wątków, ale nie być tego świadomym. Może to skończyć się dużymi problemami (które zwykle są trudne do wykrycia i odtworzenia).
jcaron

-4

Jest to dostępne w C ++ std::asyncod wersji C ++ 11.

Funkcja async funkcji szablonu uruchamia funkcję f asynchronicznie (potencjalnie w osobnym wątku, który może być częścią puli wątków) i zwraca wartość std :: future, która ostatecznie zatrzyma wynik tego wywołania funkcji.

A w C ++ 20 można używać coroutines:


5
To nie wydaje się odpowiadać na pytanie. Zgodnie z linkiem: „Co daje nam Coroutines TS? Trzy nowe słowa kluczowe w języku: co_await, co_yield i co_return” ... Ale pytanie brzmi: dlaczego w ogóle potrzebujemy await(lub co_awaitw tym przypadku) słowa kluczowego?
Arturo Torres Sánchez
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.