Warum verwalten Programmiersprachen das Synchron- / Asynchron-Problem nicht automatisch?


27

Ich habe nicht viele Ressourcen dazu gefunden: Ich habe mich gefragt, ob es möglich / eine gute Idee ist, asynchronen Code synchron schreiben zu können.

Hier ist zum Beispiel ein JavaScript-Code, der die Anzahl der in einer Datenbank gespeicherten Benutzer abruft (eine asynchrone Operation):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Es wäre schön, so etwas schreiben zu können:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

Und so würde der Compiler automatisch auf die Antwort warten und dann ausführen console.log. Es wird immer darauf gewartet, dass die asynchronen Vorgänge abgeschlossen sind, bevor die Ergebnisse an einer anderen Stelle verwendet werden müssen. Wir würden Rückrufversprechen, asynchrone / erwartete oder was auch immer, viel weniger nutzen und uns niemals Sorgen machen müssen, ob das Ergebnis einer Operation sofort verfügbar ist oder nicht.

Fehler wären nbOfUsersmit try / catch oder ähnlichen Optionen wie in der Swift- Sprache immer noch beherrschbar (haben Sie eine Ganzzahl oder einen Fehler erhalten?) .

Ist es möglich? Es kann eine schreckliche Idee / eine Utopie sein ... Ich weiß es nicht.


58
Ich verstehe deine Frage nicht wirklich. Wenn Sie "immer auf die asynchrone Operation warten", handelt es sich nicht um eine asynchrone Operation, sondern um eine synchrone Operation. Könntest Du das erläutern? Geben Sie möglicherweise die Art des Verhaltens an, nach dem Sie suchen. Außerdem ist "Was denken Sie darüber?" In der Softwareentwicklung nicht zum Thema geworden . Sie müssen Ihre Frage im Kontext eines konkreten Problems formulieren, das eine einzige, eindeutige, kanonische, objektiv korrekte Antwort hat.
Jörg W Mittag

4
@ JörgWMittag ich eine hypothetische C # vorstellen , dass implizit awaitsa Task<T>es zu konvertierenT
Caleth

6
Was Sie vorschlagen, ist nicht machbar. Es ist nicht Sache des Compilers, zu entscheiden, ob Sie das Ergebnis abwarten oder vielleicht feuern und vergessen möchten. Oder laufen Sie im Hintergrund und warten Sie später. Warum beschränken Sie sich so?
freakish

5
Ja, das ist eine schreckliche Idee. Verwenden Sie stattdessen einfach async/ await, wodurch die asynchronen Teile der Ausführung explizit werden.
Bergi

5
Wenn Sie sagen, dass zwei Dinge gleichzeitig passieren, sagen Sie, dass es in Ordnung ist, dass diese Dinge in beliebiger Reihenfolge passieren. Wenn Ihr Code keine Möglichkeit hat, klar zu machen, welche Nachbestellungen die Erwartungen Ihres Codes nicht verletzen, kann er sie nicht gleichzeitig ausführen.
Rob

Antworten:


65

Async / await ist genau das automatisierte Management, das Sie vorschlagen, allerdings mit zwei zusätzlichen Schlüsselwörtern. Warum sind sie wichtig? Abgesehen von der Abwärtskompatibilität?

  • Ohne explizite Punkte, an denen eine Coroutine ausgesetzt und wieder aufgenommen werden kann, benötigen wir ein Typsystem, um festzustellen, wo ein erwarteter Wert erwartet werden muss. Viele Programmiersprachen haben kein solches Typensystem.

  • Indem wir das Warten auf einen Wert explizit machen, können wir auch erwartbare Werte als erstklassige Objekte weitergeben: Versprechen. Dies kann beim Schreiben von Code höherer Ordnung sehr nützlich sein.

  • Asynchroner Code hat sehr tiefe Auswirkungen auf das Ausführungsmodell einer Sprache, ähnlich wie das Fehlen oder Vorhandensein von Ausnahmen in der Sprache. Insbesondere kann eine Async-Funktion nur von Async-Funktionen erwartet werden. Dies betrifft alle aufrufenden Funktionen! Was aber, wenn wir eine Funktion am Ende dieser Abhängigkeitskette von nicht asynchron auf asynchron ändern? Dies wäre eine rückwärts inkompatible Änderung ... es sei denn, alle Funktionen sind asynchron und jeder Funktionsaufruf wird standardmäßig abgewartet.

    Und das ist höchst unerwünscht, weil es sehr schlechte Auswirkungen auf die Leistung hat. Sie könnten nicht einfach günstige Werte zurückgeben. Jeder Funktionsaufruf würde viel teurer werden.

Async ist großartig, aber eine Art impliziter Async funktioniert in der Realität nicht.

Reine funktionale Sprachen wie Haskell haben eine gewisse Notlösung, da die Ausführungsreihenfolge weitgehend unbestimmt und nicht beobachtbar ist. Oder anders formuliert: Jede bestimmte Reihenfolge von Operationen muss explizit codiert werden. Dies kann für reale Programme ziemlich umständlich sein, insbesondere für Programme mit hohem E / A-Aufwand, für die Async-Code sehr gut geeignet ist.


2
Sie brauchen nicht unbedingt ein Typensystem. Transparente Futures in z. B. ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph können problemlos ohne System- oder Sprachunterstützung implementiert werden. In Smalltalk und seinen Nachkommen kann ein Objekt seine Identität transparent ändern, in ECMAScript kann es seine Form transparent ändern. Das ist alles, was Sie brauchen, um Futures transparent zu machen, ohne Sprachunterstützung für Asynchronität.
Jörg W Mittag

6
@ JörgWMittag Ich verstehe, was du sagst und wie das funktionieren könnte, aber transparente Futures ohne Typensystem machen es ziemlich schwierig, gleichzeitig erstklassige Futures zu haben, oder? Ich würde eine Möglichkeit benötigen, um auszuwählen, ob ich Nachrichten an die Zukunft oder an den Wert der Zukunft senden möchte, vorzugsweise etwas Besseres als dies, someValue ifItIsAFuture [self| self messageIWantToSend]weil es schwierig ist, mit generischem Code zu integrieren.
amon

8
@amon "Ich kann meinen asynchronen Code schreiben, da Versprechen und Versprechen Monaden sind." Monaden sind hier eigentlich nicht nötig. Thunks sind im Grunde nur Versprechungen. Da fast alle Werte in Haskell eingekästchenet sind, sind fast alle Werte in Haskell bereits Versprechungen. Das ist der Grund, warum Sie parim reinen Haskell-Code so ziemlich überall hinwerfen und kostenlos Paralellismus erhalten können.
DarthFennec

2
Async / await erinnert mich an die Fortsetzung Monade.
Les

3
Tatsächlich sind sowohl Ausnahmen als auch Async / Warten Beispiele für algebraische Effekte .
Alex Reinking

21

Was Sie vermissen, ist der Zweck von asynchronen Operationen: Sie ermöglichen es Ihnen, Ihre Wartezeit zu nutzen!

Wenn Sie eine asynchrone Operation wie das Anfordern einer Ressource von einem Server in eine synchrone Operation umwandeln, indem Sie implizit und sofort auf die Antwort warten, kann Ihr Thread die Wartezeit nicht ändern . Wenn der Server 10 Millisekunden benötigt, um zu antworten, gehen ungefähr 30 Millionen CPU-Zyklen in den Abfall. Die Wartezeit der Antwort wird zur Ausführungszeit für die Anforderung.

Der einzige Grund, warum Programmierer asynchrone Operationen erfunden haben, besteht darin, die Latenz von inhärent lang laufenden Aufgaben hinter anderen nützlichen Berechnungen zu verbergen . Wenn Sie die Wartezeit mit nützlicher Arbeit füllen können, ist das CPU-Zeit gespart. Wenn dies nicht möglich ist, geht nichts durch die asynchrone Operation verloren.

Daher empfehle ich, die von Ihren Sprachen bereitgestellten asynchronen Vorgänge zu nutzen. Sie sind da, um Ihnen Zeit zu sparen.


Ich dachte an eine funktionale Sprache, in der Operationen nicht blockieren. Selbst wenn sie eine synchrone Syntax hat, wird eine lang andauernde Berechnung den Thread nicht blockieren
Cinn

6
@Cinn Ich habe das in der Frage nicht gefunden, und das Beispiel in der Frage ist Javascript, das diese Funktion nicht hat. Im Allgemeinen ist es für einen Compiler jedoch ziemlich schwierig, sinnvolle Möglichkeiten für eine Parallelisierung zu finden, wie Sie beschreiben: Eine sinnvolle Ausnutzung eines solchen Features würde es erfordern, dass der Programmierer nach einem langen Latenzaufruf explizit darüber nachdenkt, was er korrigiert. Wenn Sie die Laufzeitumgebung intelligent genug gestalten, um diese Anforderung an den Programmierer zu umgehen, wird Ihre Laufzeit wahrscheinlich die Leistungsersparnis aufzehren, da sie über Funktionsaufrufe hinweg aggressiv parallelisiert werden müsste.
cmaster

2
Alle Computer warten mit der gleichen Geschwindigkeit.
Bob Jarvis - Setzen Sie Monica

2
@ BobJarvis Ja. Aber sie unterscheiden sich in , wie viel Arbeit sie könnten in der Wartezeit getan haben ...
cMaster

13

Einige tun.

Sie sind (noch) kein Mainstream, weil Async ein relativ neues Feature ist, für das wir erst jetzt ein gutes Gefühl bekommen haben, ob es überhaupt ein gutes Feature ist oder wie man es Programmierern auf eine Art und Weise präsentiert, die freundlich / benutzbar / ausdrucksstark / etc. Vorhandene asynchrone Funktionen sind weitgehend an vorhandene Sprachen gebunden, was einen etwas anderen Entwurfsansatz erfordert.

Trotzdem ist es nicht unbedingt eine gute Idee, überall etwas zu unternehmen. Ein häufiger Fehler besteht darin, asynchrone Aufrufe in einer Schleife auszuführen und ihre Ausführung effektiv zu serialisieren. Implizite asynchrone Aufrufe können diese Art von Fehlern verschleiern. Wenn Sie implizite Nötigung von a Task<T>(oder einer Entsprechung Ihrer Sprache) zu Tunterstützen, kann dies zu einer gewissen Komplexität / Kosten für Ihre Typechecker- und Fehlerberichterstattung führen, wenn nicht klar ist, welche der beiden vom Programmierer wirklich gewünscht wurde.

Das sind aber keine unüberwindlichen Probleme. Wenn Sie dieses Verhalten unterstützen wollten, könnten Sie es mit ziemlicher Sicherheit, obwohl es Kompromisse geben würde.


1
Ich denke, eine Idee könnte sein, alles in asynchrone Funktionen zu packen, die synchronen Aufgaben würden sich sofort auflösen und wir bekommen alle eine Art zu handhaben (Edit: @amon erklärt, warum es eine schlechte Idee ist ...)
Cinn

8
Können Sie ein paar Beispiele für „geben Manche tun “, bitte?
Bergi

2
Asynchrone Programmierung ist in keiner Weise neu, heutzutage muss man sich nur öfter damit auseinandersetzen.
Cubic

1
@ Cubic - es ist, soweit ich weiß, ein Sprachfeature. Vorher gab es nur (umständliche) Userland-Funktionen.
Telastyn

12

Es gibt Sprachen, die dies tun. Es besteht jedoch eigentlich kein großer Bedarf, da dies mit vorhandenen Sprachfunktionen leicht erreicht werden kann.

Solange Sie haben eine gewisse Art und Weise auszudrücken Asynchronität, können Sie implementieren Futures oder Versprechungen rein als Bibliothek Feature, Sie keine speziellen Sprachfunktionen benötigen. Und solange Sie haben einige auszudrücken Transparente Proxies , können Sie die beiden Funktionen zusammen , und Sie haben Transparent Futures .

Beispielsweise kann in Smalltalk und seinen Nachkommen ein Objekt seine Identität ändern, es kann buchstäblich ein anderes Objekt werden (und tatsächlich wird die Methode, die dies ausführt, aufgerufen Object>>become:).

Stellen Sie sich eine lang andauernde Berechnung vor, die a zurückgibt Future<Int>. Dies Future<Int>hat alle die gleichen Methoden Int, außer mit unterschiedlichen Implementierungen. Future<Int>Bei der +Methode von wird keine weitere Zahl hinzugefügt und das Ergebnis zurückgegeben. Es wird eine neue zurückgegeben, Future<Int>die die Berechnung umschließt. Und so weiter und so fort. Methoden, die durch Rückgabe von a nicht sinnvoll implementiert werden können Future<Int>, werden stattdessen automatisch awaitdas Ergebnis und anschließend der Aufruf self become: result., der das aktuell ausgeführte Objekt ( selfdh Future<Int>das resultObjekt ) buchstäblich zum Objekt macht, dh von nun an die Objektreferenz, die früher ein Future<Int>ist jetzt ein Intüberall, völlig transparent für den Kunden.

Es sind keine speziellen asynchronen Sprachfunktionen erforderlich.


OK, aber das hat Probleme, wenn beide Future<T>und Teinige gemeinsame Schnittstelle und ich Funktionen von dieser Schnittstelle verwenden. Soll es becomedas Ergebnis sein und dann die Funktionalität nutzen, oder nicht? Ich denke an Dinge wie einen Gleichheitsoperator oder eine Debug-Darstellung in Form eines To-Strings.
amon

Ich verstehe, dass es keine Features hinzufügt. Wir haben verschiedene Syntaxen, um Berechnungen mit sofortiger Auflösung und Berechnungen mit langer Laufzeit zu schreiben. Danach würden wir die Ergebnisse auf die gleiche Weise für andere Zwecke verwenden. Ich habe mich gefragt, ob wir eine Syntax haben könnten, die beide transparent behandelt, so dass sie besser lesbar ist und der Programmierer nicht damit umgehen muss. Wie bei a + bbeiden ganzen Zahlen spielt es keine Rolle, ob a und b sofort oder später verfügbar sind. Wir schreiben einfach a + b(machen es möglich Int + Future<Int>)
Cinn

@Cinn: Ja, das können Sie mit Transparent Futures tun, und dafür benötigen Sie keine speziellen Sprachfunktionen. Sie können es mit den bereits vorhandenen Funktionen implementieren, z. B. in Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript und anscheinend, wie ich gerade gelesen habe, in Python.
Jörg W Mittag

3
@amon: Die Idee von Transparent Futures ist, dass Sie nicht wissen, dass es eine Zukunft ist. Aus Ihrer Sicht gibt es keine gemeinsame Schnittstelle zwischen Future<T>und Taus Ihrer Sicht gibt es keineFuture<T> , nur eine T. Jetzt gibt es natürlich viele technische Herausforderungen, wie dies effizient gestaltet werden kann, welche Vorgänge blockiert oder nicht blockiert werden sollen usw., aber das ist wirklich unabhängig davon, ob Sie es als Sprache oder als Bibliotheksfeature ausführen. Transparenz war eine Anforderung, die vom OP in der Frage festgelegt wurde. Ich werde nicht behaupten, dass es schwierig ist und möglicherweise keinen Sinn ergibt.
Jörg W Mittag

3
@ Jörg Das scheint alles andere als in funktionalen Sprachen problematisch zu sein, da man nicht weiß, wann Code tatsächlich in diesem Modell ausgeführt wird. Das funktioniert in Haskell im Allgemeinen gut, aber ich kann nicht sehen, wie dies in prozeduralen Sprachen funktionieren würde (und selbst in Haskell muss man manchmal eine Ausführung erzwingen und das zugrunde liegende Modell verstehen, wenn man sich um die Leistung kümmert). Trotzdem eine interessante Idee.
Voo

7

Sie tun (nun, die meisten von ihnen). Die gesuchte Funktion heißt Threads .

Threads haben jedoch ihre eigenen Probleme:

  1. Da der Code jederzeit ausgesetzt werden kann , können Sie niemals davon ausgehen, dass sich die Dinge nicht "von selbst" ändern. Wenn Sie mit Threads programmieren, denken Sie viel Zeit darüber nach, wie Ihr Programm mit Änderungen umgehen soll.

    Stellen Sie sich vor, ein Spielserver verarbeitet den Angriff eines Spielers auf einen anderen Spieler. Etwas wie das:

    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();
    }
    

    Drei Monate später entdeckt ein Spieler, dass er seine Gegenstände behalten kann, wenn er getötet wird und sich attacker.addInventoryItemsim laufenden Betrieb victim.removeInventoryItemsabmeldet. Der Angreifer erhält auch eine Kopie seiner Gegenstände. Er tut dies mehrere Male, indem er aus dem Nichts eine Million Tonnen Gold erschafft und die Wirtschaft des Spiels zum Erliegen bringt.

    Alternativ kann sich der Angreifer abmelden, während das Spiel eine Nachricht an das Opfer sendet, und er bekommt keinen "Mörder" -Tag über dem Kopf, damit sein nächstes Opfer nicht vor ihm davonläuft.

  2. Da der Code jederzeit ausgesetzt werden kann , müssen Sie beim Bearbeiten von Datenstrukturen überall Sperren verwenden. Ich habe oben ein Beispiel angeführt, das offensichtliche Konsequenzen für ein Spiel hat, aber subtiler sein kann. Erwägen Sie, dem Anfang einer verknüpften Liste ein Element hinzuzufügen:

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

    Dies ist kein Problem, wenn Sie sagen, dass Threads nur dann angehalten werden können, wenn sie E / A ausführen, und nicht zu irgendeinem Zeitpunkt. Aber ich bin sicher, Sie können sich eine Situation vorstellen, in der es eine E / A-Operation gibt - wie zum Beispiel die Protokollierung:

    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. Da der Code kann ausgesetzt werden jedem Punkt , könnte es möglicherweise viel Staat zu retten. Das System behandelt dies, indem es jedem Thread einen völlig separaten Stapel gibt. Der Stapel ist jedoch ziemlich groß, sodass Sie in einem 32-Bit-Programm nicht mehr als etwa 2000 Threads haben können. Sie können auch die Stapelgröße reduzieren, wenn die Gefahr besteht, dass der Stapel zu klein wird.


3

Viele der hier gegebenen Antworten sind irreführend, da die Frage zwar buchstäblich nach asynchroner Programmierung und nicht nach blockierungsfreiem E / A gestellt wurde, wir aber in diesem speziellen Fall nicht über das eine diskutieren können, ohne das andere zu diskutieren.

Während asynchrone Programmierung von Natur aus asynchron ist, besteht das Hauptziel asynchroner Programmierung darin, das Blockieren von Kernel-Threads zu vermeiden. Node.js verwendet Asynchronität über Callbacks oder Promises, um zu ermöglichen, dass Blockierungsvorgänge von einer Ereignisschleife ausgelöst werden, und Netty in Java verwendet Asynchronität über Callbacks oder CompletableFutures, um etwas Ähnliches zu tun.

Nicht blockierender Code erfordert jedoch keine Asynchronität . Es kommt darauf an, wie viel Ihre Programmiersprache und Laufzeit bereit ist, für Sie zu tun.

Go, Erlang und Haskell / GHC können dies für Sie erledigen. Sie können so etwas wie schreiben var response = http.get('example.com/test')und einen Kernel-Thread hinter den Kulissen veröffentlichen lassen, während Sie auf eine Antwort warten. Dies geschieht durch Goroutinen, Erlang-Prozesse oder das forkIOLoslassen von Kernel-Threads hinter den Kulissen beim Blockieren, sodass andere Aktionen ausgeführt werden können, während auf eine Antwort gewartet wird .

Es ist wahr, dass die Sprache nicht wirklich mit Asynchronität umgehen kann, aber einige Abstraktionen lassen Sie weiter gehen als andere, z. B. unbegrenzte Fortsetzungen oder asymmetrische Koroutinen. Die Hauptursache für asynchronen Code, das Blockieren von Systemaufrufen, kann jedoch absolut vom Entwickler entfernt werden.

Node.js und Java unterstützen asynchronen, nicht blockierenden Code, wohingegen Go und Erlang synchronen, nicht blockierenden Code unterstützen. Sie sind beide gültige Ansätze mit unterschiedlichen Kompromissen.

Mein eher subjektives Argument ist, dass diejenigen, die sich gegen Laufzeiten aussprechen, die im Auftrag des Entwicklers nicht blockieren, sich gegen die Müllabfuhr in den frühen Neunzigern aussprechen. Ja, es verursacht Kosten (in diesem Fall in erster Linie mehr Speicher), erleichtert jedoch die Entwicklung und das Debuggen und macht Codebasen robuster.

Ich persönlich würde argumentieren, dass asynchroner, nicht blockierender Code in Zukunft für die Systemprogrammierung reserviert werden sollte und modernere Technologie-Stacks für die Anwendungsentwicklung auf synchrone, nicht blockierende Laufzeiten migriert werden sollten .


1
Das war eine wirklich interessante Antwort! Ich bin mir jedoch nicht sicher, ob ich Ihre Unterscheidung zwischen "synchronem" und "asynchronem" nicht blockierendem Code verstehe. Synchroner nicht blockierender Code bedeutet für mich, dass so etwas wie eine C-Funktion waitpid(..., WNOHANG)fehlschlägt, wenn sie blockieren müsste. Oder bedeutet "synchron" hier "es gibt keine vom Programmierer sichtbaren Rückrufe / Zustandsautomaten / Ereignisschleifen"? Aber für Ihr Go-Beispiel muss ich immer noch explizit auf ein Ergebnis einer Goroutine warten, indem ich aus einem Kanal lese, nicht wahr? Wie ist das weniger asynchron als asynchron / wait in JS / C # / Python?
amon

1
Ich benutze "asynchron" und "synchron", um das Programmiermodell zu besprechen, das dem Entwickler zur Verfügung gestellt wird, und "Blockieren" und "Nichtblockieren", um das Blockieren eines Kernel-Threads zu besprechen, bei dem es nichts Sinnvolles tun kann, selbst wenn es solche gibt andere Berechnungen, die durchgeführt werden müssen, und es gibt einen freien logischen Prozessor, den es verwenden kann. Nun, eine Goroutine kann einfach auf ein Ergebnis warten, ohne den zugrunde liegenden Thread zu blockieren, aber eine andere Goroutine kann über einen Kanal mit ihr kommunizieren, wenn sie dies wünscht. Die Goroutine muss keinen Kanal direkt verwenden , um auf ein nicht blockierendes Lesen des Sockets zu warten.
Louis Jackman

Hmm ok, ich verstehe deine Unterscheidung jetzt. Während es mir mehr um die Verwaltung des Daten- und Kontrollflusses zwischen Coroutinen geht, geht es Ihnen eher darum, den Hauptkernel-Thread niemals zu blockieren. Ich bin mir nicht sicher, ob Go oder Haskell in dieser Hinsicht einen Vorteil gegenüber C ++ oder Java haben, da auch sie Hintergrund-Threads auslösen können, was nur ein bisschen mehr Code erfordert.
amon

@LouisJackman könnte ein wenig auf Ihre letzte Aussage zum asynchronen Nicht-Blockieren für die Systemprogrammierung eingehen. Was sind die Vorteile eines asynchronen, nicht blockierenden Ansatzes?
Sunprophit

@sunprophit Asynchrones Nicht-Blockieren ist nur eine Compiler-Transformation (normalerweise asynchron / wait), wohingegen synchrones Nicht-Blockieren Laufzeitunterstützung wie eine Kombination aus komplexer Stapelmanipulation, Einfügen von Fließpunkten bei Funktionsaufrufen (die mit Inlining kollidieren können), Nachverfolgen erfordert. “ Reduktionen “(für die eine VM wie BEAM erforderlich ist) usw. Wie bei der Garbage Collection wird weniger Laufzeitkomplexität für Benutzerfreundlichkeit und Robustheit in Kauf genommen. Systemsprachen wie C, C ++ und Rust vermeiden aufgrund ihrer Zieldomänen größere Laufzeitfeatures wie dieses, sodass asynchrones Nicht-Blockieren dort sinnvoller ist.
Louis Jackman

2

Wenn ich Sie richtig verstehe, fordern Sie ein synchrones Programmiermodell, aber eine hochperformante Implementierung. Wenn das stimmt, steht uns das bereits in Form von grünen Fäden oder Prozessen von zB Erlang oder Haskell zur Verfügung. Also ja, es ist eine hervorragende Idee, aber die Nachrüstung bestehender Sprachen kann nicht immer so reibungslos vonstatten gehen, wie Sie es möchten.


2

Ich schätze die Frage und finde, dass die Mehrheit der Antworten lediglich den Status Quo verteidigt. Im Spektrum der Sprachen auf niedriger bis hoher Ebene stecken wir seit einiger Zeit in Schwierigkeiten. Die nächsthöhere Ebene wird eindeutig eine Sprache sein, die sich weniger auf die Syntax konzentriert (die Notwendigkeit expliziter Schlüsselwörter wie wait und async) und viel mehr auf die Absicht. (Offensichtliche Anerkennung an Charles Simonyi, aber ich denke an 2019 und die Zukunft.)

Wenn ich einem Programmierer erzähle, dass er Code schreiben soll, der einfach einen Wert aus einer Datenbank abruft, können Sie davon ausgehen, dass ich meine, "und BTW, hängen Sie die Benutzeroberfläche nicht auf" und "keine anderen Überlegungen einführen, die schwer zu findende Fehler überdecken ". Programmierer der Zukunft, die über eine neue Generation von Sprachen und Werkzeugen verfügen, werden sicherlich in der Lage sein, Code zu schreiben, der einfach einen Wert in einer Codezeile abruft und von dort aus weitergeht.

Die Sprache auf höchster Ebene ist Englisch, und Sie können sich auf die Kompetenz des Auftraggebers verlassen, um zu wissen, was Sie wirklich tun möchten. (Denken Sie an den Computer in Star Trek oder fragen Sie Alexa.) Wir sind weit davon entfernt, aber nähern uns dem, und ich gehe davon aus, dass die Sprache / der Compiler mehr dazu geeignet sein könnte, robusten, beabsichtigten Code zu generieren, ohne dies zu tun AI brauchen.

Einerseits gibt es neuere visuelle Sprachen wie Scratch, die dies tun und nicht mit allen syntaktischen Techniken überfordert sind. Natürlich wird viel hinter den Kulissen gearbeitet, damit sich der Programmierer keine Sorgen machen muss. Das heißt, ich schreibe keine Business-Class-Software in Scratch. Daher gehe ich wie Sie davon aus, dass es an der Zeit ist, dass ausgereifte Programmiersprachen das Synchron- / Asynchron-Problem automatisch lösen.


1

Das Problem, das Sie beschreiben, ist zweifach.

  • Das von Ihnen geschriebene Programm sollte sich von außen betrachtet insgesamt asynchron verhalten .
  • An der Aufrufstelle sollte nicht sichtbar sein, ob ein Funktionsaufruf die Kontrolle möglicherweise aufgibt oder nicht.

Es gibt ein paar Möglichkeiten, dies zu erreichen, aber im Grunde läuft es darauf hinaus

  1. mit mehreren Threads (auf einer bestimmten Abstraktionsebene)
  2. auf der Sprachebene mehrere Arten von Funktionen haben, die alle so genannt werden foo(4, 7, bar, quux).

Für (1) fasse ich mehrere Prozesse zusammen, führe mehrere Kernel-Threads und Green-Thread-Implementierungen aus, die Threads auf Sprachlaufzeitebene für Kernel-Threads planen. Aus der Sicht des Problems sind sie gleich. In dieser Welt gibt keine Funktion jemals die Kontrolle aus der Perspektive ihres Threads auf oder verliert sie . Der Thread selbst hat manchmal keine Kontrolle und läuft manchmal nicht, aber Sie geben die Kontrolle über Ihren eigenen Thread in dieser Welt nicht auf. Ein System, das diesem Modell entspricht, kann möglicherweise neue Threads erzeugen oder vorhandene Threads verbinden. Ein System, das zu diesem Modell passt, ist möglicherweise nicht in der Lage, einen Thread wie den von Unix zu duplizierenfork .

(2) ist interessant. Um dem gerecht zu werden, müssen wir über Einführungs- und Ausscheidungsformulare sprechen.

Ich werde zeigen, warum implizit awaitnicht auf abwärtskompatible Weise zu einer Sprache wie Javascript hinzugefügt werden kann. Die Grundidee ist, dass Javascript durch die Offenlegung von Versprechungen für den Benutzer und die Unterscheidung zwischen synchronen und asynchronen Kontexten ein Implementierungsdetail preisgibt, das die einheitliche Behandlung synchroner und asynchroner Funktionen verhindert. Es gibt auch die Tatsache, dass Sie kein awaitVersprechen außerhalb eines Körpers mit asynchroner Funktion geben können. Diese Entwurfswahlen sind nicht kompatibel mit "Asynchronität für den Aufrufer unsichtbar machen".

Sie können eine synchrone Funktion mit einem Lambda einführen und mit einem Funktionsaufruf beseitigen.

Einführung in die Synchronfunktion:

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

Synchrone Funktionsbeseitigung:

f(4)

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

Sie können dies mit der Einführung und Beseitigung asynchroner Funktionen kontrastieren.

Einführung in die asynchrone Funktion

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

Eliminierung asynchroner Funktionen (Hinweis: Nur innerhalb einer asyncFunktion gültig )

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

Das grundlegende Problem hierbei ist, dass eine asynchrone Funktion auch eine synchrone Funktion ist, die ein Versprechungsobjekt erzeugt .

Hier ist ein Beispiel für den synchronen Aufruf einer asynchronen Funktion in der node.js-Replikation.

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

Sie können hypothetisch eine Sprache haben, auch eine dynamisch typisierte, bei der der Unterschied zwischen asynchronen und synchronen Funktionsaufrufen auf der Aufrufsite nicht sichtbar und möglicherweise auf der Definitionssite nicht sichtbar ist.

Wenn Sie eine solche Sprache auf Javascript reduzieren, müssen Sie lediglich alle Funktionen asynchronisieren.


1

Mit den Goroutinen der Sprache Go und der Laufzeit der Sprache Go können Sie den gesamten Code so schreiben, als wäre er synchronisiert. Wenn eine Operation in einer Goroutine blockiert wird, wird die Ausführung in anderen Goroutinen fortgesetzt. Und mit Kanälen können Sie problemlos zwischen Goroutinen kommunizieren. Dies ist oft einfacher als Rückrufe in Javascript oder async / await in anderen Sprachen. Unter https://tour.golang.org/concurrency/1 finden Sie einige Beispiele und eine Erklärung.

Außerdem habe ich keine persönlichen Erfahrungen damit, aber ich habe gehört, dass Erlang ähnliche Einrichtungen hat.

Also, ja, es gibt Programmiersprachen wie Go und Erlang, die das synchron / asynchrone Problem lösen, aber leider sind sie noch nicht sehr beliebt. Da diese Sprachen immer beliebter werden, werden die von ihnen bereitgestellten Funktionen möglicherweise auch in anderen Sprachen implementiert.


Ich habe fast nie die Sprache Go verwendet, aber es scheint, dass Sie explizit deklarieren go ..., so sieht es ähnlich aus wie await ...nein?
Cinn

1
@Cinn Eigentlich nein. Sie können jeden Anruf als Goroutine auf eine eigene Faser / einen eigenen grünen Faden mit setzen go. Und so gut wie jeder Aufruf, der blockiert werden könnte, wird von der Laufzeit asynchron ausgeführt, die in der Zwischenzeit nur auf eine andere Goroutine umschaltet (kooperatives Multitasking). Sie warten auf eine Nachricht.
Deduplizierer

2
Während Goroutines eine Art Nebenläufigkeit sind, würde ich sie nicht in den gleichen Eimer wie async / await legen: keine kooperativen Coroutines, sondern automatisch (und präventiv!) Geplante grüne Threads. Aber das Warten wird auch nicht automatisch: Go entspricht dem awaitLesen von einem Kanal <- ch.
amon

@amon Soweit ich weiß, werden Goroutinen auf systemeigenen Threads (normalerweise gerade genug, um die echte Hardware-Parallelität zu maximieren) kooperativ zur Laufzeit geplant, und diese werden vom Betriebssystem vorab geplant.
Deduplizierer

Das OP forderte "asynchronen Code synchron schreiben zu können". Wie Sie bereits erwähnt haben, können Sie dies mit Goroutinen und der go-Laufzeit genau tun. Sie müssen sich nicht um die Details des Threading kümmern, sondern schreiben nur blockierende Lese- und Schreibvorgänge, als ob der Code synchron wäre, und Ihre anderen Goroutinen, falls vorhanden, werden weiter ausgeführt. Sie müssen auch nicht einmal "warten" oder aus einem Kanal lesen, um diesen Vorteil zu erhalten. Ich denke daher, dass Go eine Programmiersprache ist, die den Wünschen des OP am ehesten entspricht.

1

Es gibt einen sehr wichtigen Aspekt, der noch nicht angesprochen wurde: Wiedereintritt. Wenn Sie einen anderen Code (dh eine Ereignisschleife) haben, der während des asynchronen Aufrufs ausgeführt wird (und wenn Sie dies nicht tun, warum benötigen Sie dann überhaupt asynchronen Code?), Kann sich der Code auf den Programmstatus auswirken. Sie können die asynchronen Aufrufe nicht vor dem Aufrufer verbergen, da der Aufrufer möglicherweise davon abhängt, dass Teile des Programmstatus für die Dauer seines Funktionsaufrufs unberührt bleiben. Beispiel:

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

Wenn bar()es sich um eine asynchrone Funktion handelt, kann sich diese möglicherweise obj.xwährend der Ausführung ändern. Dies wäre ziemlich unerwartet, ohne den Hinweis, dass der Balken asynchron ist und dieser Effekt möglich ist. Die einzige Alternative wäre, zu vermuten, dass jede mögliche Funktion / Methode asynchron ist, und einen nicht lokalen Status nach jedem Funktionsaufruf erneut abzurufen und zu überprüfen. Dies ist anfällig für subtile Fehler und möglicherweise überhaupt nicht möglich, wenn ein Teil des nicht lokalen Status über Funktionen abgerufen wird. Aus diesem Grund muss der Programmierer wissen, welche der Funktionen das Potenzial haben, den Programmstatus auf unerwartete Weise zu ändern:

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

Jetzt ist klar ersichtlich, dass es sich bei der bar()Funktion um eine asynchrone Funktion handelt. Die richtige Vorgehensweise besteht darin, den erwarteten Wert von obj.xanschließend erneut zu überprüfen und etwaige Änderungen zu berücksichtigen.

Wie bereits in anderen Antworten erwähnt, können sich reine Funktionssprachen wie Haskell diesem Effekt vollständig entziehen, indem sie die Notwendigkeit eines gemeinsamen / globalen Zustands überhaupt vermeiden. Ich habe nicht viel Erfahrung mit funktionalen Sprachen, daher bin ich wahrscheinlich voreingenommen, aber ich denke nicht, dass das Fehlen des globalen Zustands ein Vorteil ist, wenn ich größere Anwendungen schreibe.


0

Im Fall von Javascript, das Sie in Ihrer Frage verwendet haben, ist Folgendes zu beachten: Javascript ist ein Singlethread und die Ausführungsreihenfolge ist garantiert, solange keine asynchronen Aufrufe vorliegen.

Wenn Sie also eine Sequenz wie Ihre haben:

const nbOfUsers = getNbOfUsers();

Sie werden garantiert, dass in der Zwischenzeit nichts anderes ausgeführt wird. Keine Notwendigkeit für Schlösser oder ähnliches.

Wenn getNbOfUsersjedoch asynchron ist, gilt Folgendes:

const nbOfUsers = await getNbOfUsers();

bedeutet, dass während getNbOfUsersder Ausführung möglicherweise Ausführungsergebnisse und anderer Code dazwischen ausgeführt werden. Dies kann wiederum eine Sperrung erfordern, je nachdem, was Sie tun.

Es ist daher eine gute Idee, sich darüber im Klaren zu sein, wann ein Anruf asynchron ist und wann nicht, da Sie in bestimmten Situationen zusätzliche Vorsichtsmaßnahmen treffen müssen, die Sie bei einem synchronen Anruf nicht treffen müssten.


Sie haben Recht, mein zweiter Code in der Frage ist ungültig, als ob er getNbOfUsers()ein Versprechen zurückgibt. Aber genau das ist der Punkt meiner Frage, warum müssen wir es explizit als asynchron schreiben, der Compiler könnte es erkennen und es automatisch auf eine andere Art und Weise handhaben.
Cinn

@Cinn das ist nicht mein Punkt. Mein Punkt ist, dass der Ausführungsfluss während der Ausführung des asynchronen Aufrufs möglicherweise zu anderen Teilen Ihres Codes gelangt, während es für einen synchronen Aufruf nicht möglich ist. Es wäre so, als würden mehrere Threads ausgeführt, ohne sich dessen bewusst zu sein. Dies kann zu großen Problemen führen (die normalerweise schwer zu erkennen und zu reproduzieren sind).
jcaron

-4

Dies ist in C ++ wie std::asyncseit C ++ 11 verfügbar .

Die Vorlagenfunktion async führt die Funktion f asynchron aus (möglicherweise in einem separaten Thread, der Teil eines Thread-Pools sein kann) und gibt eine std :: future zurück, die schließlich das Ergebnis dieses Funktionsaufrufs enthält.

Und mit C ++ 20 können Coroutinen verwendet werden:


5
Dies scheint die Frage nicht zu beantworten. Laut Ihrem Link: "Was gibt uns die Coroutines TS? Drei neue Sprachschlüsselwörter: co_await, co_yield und co_return" ... Aber die Frage ist, warum wir überhaupt ein await(oder co_awaitin diesem Fall) Schlüsselwort benötigen ?
Arturo Torres Sánchez
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.