Das asynchrone Wartemuster von .net 4.5 ändert das Paradigma. Es ist fast zu schön um wahr zu sein.
Ich habe einen E / A-lastigen Code auf async-await portiert, da das Blockieren der Vergangenheit angehört.
Nicht wenige Leute vergleichen asynchrones Warten mit einem Zombie-Befall und ich fand es ziemlich genau. Async-Code mag anderen Async-Code (Sie benötigen eine Async-Funktion, um auf eine Async-Funktion zu warten). So werden immer mehr Funktionen asynchron und dies wächst in Ihrer Codebasis weiter.
Das Ändern von Funktionen in Async ist eine sich wiederholende und einfallslose Arbeit. Wenn Sie ein async
Schlüsselwort in die Deklaration einfügen, den Rückgabewert mit Task<>
einschließen, sind Sie ziemlich fertig. Es ist ziemlich beunruhigend, wie einfach der gesamte Prozess ist, und ziemlich bald wird ein Textersetzungsskript den größten Teil der "Portierung" für mich automatisieren.
Und jetzt die Frage: Wenn mein gesamter Code langsam asynchron wird, warum nicht einfach alles standardmäßig asynchron machen?
Der offensichtliche Grund, den ich annehme, ist die Leistung. Async-await hat einen Overhead und Code, der nicht asynchron sein muss, vorzugsweise nicht. Wenn jedoch die Leistung das einzige Problem ist, können einige clevere Optimierungen den Overhead automatisch entfernen, wenn er nicht benötigt wird. Ich habe über die "Fast Path" -Optimierung gelesen , und es scheint mir, dass sie sich allein um das meiste kümmern sollte.
Vielleicht ist dies vergleichbar mit dem Paradigmenwechsel der Müllsammler. In den frühen GC-Tagen war es definitiv effizienter, das eigene Gedächtnis freizugeben. Aber die Massen entschieden sich immer noch für die automatische Erfassung zugunsten eines sichereren, einfacheren Codes, der möglicherweise weniger effizient ist (und selbst das ist wohl nicht mehr wahr). Vielleicht sollte dies hier der Fall sein? Warum sollten nicht alle Funktionen asynchron sein?
quelle
async
(um einen Vertrag zu erfüllen), ist dies wahrscheinlich eine schlechte Idee. Sie haben die Nachteile von Async (erhöhte Kosten für Methodenaufrufe; die Notwendigkeit,await
den aufrufenden Code zu verwenden), aber keinen der Vorteile.Antworten:
Zunächst einmal vielen Dank für Ihre freundlichen Worte. Es ist in der Tat eine großartige Funktion und ich bin froh, ein kleiner Teil davon gewesen zu sein.
Nun, Sie übertreiben; alle ist der Code nicht async drehen. Wenn Sie zwei "einfache" Ganzzahlen addieren, warten Sie nicht auf das Ergebnis. Wenn Sie zwei zukünftige Ganzzahlen addieren , um eine dritte zukünftige Ganzzahl zu erhalten - denn das
Task<int>
ist es, es ist eine Ganzzahl, auf die Sie in Zukunft zugreifen werden -, werden Sie wahrscheinlich auf das Ergebnis warten.Der Hauptgrund, nicht alles asynchron zu machen, liegt darin, dass der Zweck von async / await darin besteht, das Schreiben von Code in einer Welt mit vielen Operationen mit hoher Latenz zu vereinfachen . Die überwiegende Mehrheit Ihrer Vorgänge weist keine hohe Latenz auf. Daher ist es nicht sinnvoll, den Leistungseinbruch zu berücksichtigen, der diese Latenz verringert. Vielmehr ist ein Schlüssel wenige sind Ihre Operationen mit hohen Latenz, und diese Operationen im gesamten Code den Zombie - Befall von Asynchron verursachen.
In Theorie, Theorie und Praxis sind sie ähnlich. In der Praxis sind sie es nie.
Lassen Sie mich drei Punkte gegen diese Art der Transformation geben, gefolgt von einem Optimierungsdurchlauf.
Der erste Punkt ist erneut: Async in C # / VB / F # ist im Wesentlichen eine begrenzte Form der Weitergabe . In der Community der funktionalen Sprachen wurde eine enorme Menge an Forschung betrieben, um herauszufinden, wie Code optimiert werden kann, bei dem der Continuation-Passing-Stil stark genutzt wird. Das Compilerteam müsste wahrscheinlich sehr ähnliche Probleme in einer Welt lösen, in der "asynchron" die Standardeinstellung war und die nicht asynchronen Methoden identifiziert und de-asynchronisiert werden mussten. Das C # -Team ist nicht wirklich daran interessiert, offene Forschungsprobleme anzunehmen, also sind das große Punkte gegen genau dort.
Ein zweiter Punkt dagegen ist, dass C # nicht über die "referenzielle Transparenz" verfügt, die diese Art von Optimierungen leichter nachvollziehbar macht. Mit "referentieller Transparenz" meine ich die Eigenschaft, dass der Wert eines Ausdrucks nicht davon abhängt, wann er ausgewertet wird . Ausdrücke wie
2 + 2
sind referenziell transparent; Sie können die Auswertung zur Kompilierungszeit durchführen, wenn Sie möchten, oder sie bis zur Laufzeit verschieben und die gleiche Antwort erhalten. Ein Ausdruck wiex+y
kann jedoch nicht rechtzeitig verschoben werden, da sich x und y im Laufe der Zeit ändern können .Async macht es viel schwieriger zu überlegen, wann eine Nebenwirkung auftreten wird. Vor dem Async, wenn Sie sagten:
und
M()
warvoid M() { Q(); R(); }
,N()
warvoid N() { S(); T(); }
undR
undS
erzeugt Nebenwirkungen, dann wissen Sie, dass die Nebenwirkung von R vor der Nebenwirkung von S auftritt. Aber wenn Sieasync void M() { await Q(); R(); }
dann plötzlich haben, geht das aus dem Fenster. Sie haben keine Garantie dafür, obR()
dies vorher oder nachher geschehen wirdS()
(es sei denn, es wird natürlichM()
erwartet; aber natürlichTask
muss es erst danach erwartet werdenN()
.)Stellen Sie sich nun vor, dass diese Eigenschaft, nicht mehr zu wissen, in welcher Reihenfolge Nebenwirkungen auftreten, für jeden Code in Ihrem Programm gilt, mit Ausnahme derjenigen, die der Optimierer de-asynchronisieren kann. Grundsätzlich haben Sie keine Ahnung mehr, welche Ausdrücke in welcher Reihenfolge ausgewertet werden. Dies bedeutet, dass alle Ausdrücke referenziell transparent sein müssen, was in einer Sprache wie C # schwierig ist.
Ein dritter Punkt dagegen ist, dass Sie sich dann fragen müssen: "Warum ist Async so besonders?" Wenn Sie argumentieren wollen, dass jede Operation tatsächlich eine sein sollte, müssen
Task<T>
Sie in der Lage sein, die Frage "Warum nichtLazy<T>
?" Zu beantworten. oder "warum nichtNullable<T>
?" oder "warum nichtIEnumerable<T>
?" Weil wir das genauso gut machen könnten. Warum sollte es nicht so sein, dass jede Operation auf nullable angehoben wird ? Oder jede Operation wird träge berechnet und das Ergebnis für später zwischengespeichert , oder das Ergebnis jeder Operation ist eine Folge von Werten anstelle nur eines einzelnen Werts . Sie müssen dann versuchen, Situationen zu optimieren, in denen Sie wissen: "Oh, das darf niemals null sein, damit ich besseren Code generieren kann" und so weiter.Der springende Punkt ist: Mir
Task<T>
ist nicht klar, dass das wirklich so besonders ist, um so viel Arbeit zu rechtfertigen.Wenn Sie an solchen Dingen interessiert sind, empfehle ich Ihnen, funktionale Sprachen wie Haskell zu untersuchen, die eine viel stärkere referenzielle Transparenz aufweisen und alle Arten von Auswertungen außerhalb der Reihenfolge ermöglichen und automatisches Caching durchführen. Haskell hat auch eine viel stärkere Unterstützung in seinem Typensystem für die Art von "monadischen Aufzügen", auf die ich angespielt habe.
quelle
async/await
weil etwas unterhalb Abstraktionsschichten versteckt könnte asynchron sein.Leistung ist ein Grund, wie Sie erwähnt haben. Beachten Sie, dass die Option "Schneller Pfad", mit der Sie verknüpft sind, die Leistung bei einer abgeschlossenen Aufgabe verbessert, jedoch im Vergleich zu einem einzelnen Methodenaufruf immer noch viel mehr Anweisungen und Overhead erfordert. Selbst mit dem "schnellen Pfad" erhöhen Sie mit jedem asynchronen Methodenaufruf die Komplexität und den Overhead.
Die Abwärtskompatibilität sowie die Kompatibilität mit anderen Sprachen (einschließlich Interop-Szenarien) würden ebenfalls problematisch.
Der andere ist eine Frage der Komplexität und Absicht. Asynchrone Operationen erhöhen die Komplexität - in vielen Fällen verbergen die Sprachfunktionen dies, aber es gibt viele Fälle, in denen Methoden erstellt werden
async
ihre Verwendung definitiv komplexer macht. Dies gilt insbesondere dann, wenn Sie keinen Synchronisationskontext haben, da die asynchronen Methoden dann leicht zu unerwarteten Threading-Problemen führen können.Darüber hinaus gibt es viele Routinen, die von Natur aus nicht asynchron sind. Diese sind als synchrone Operationen sinnvoller. Erzwingen
Math.Sqrt
zu seinTask<double> Math.SqrtAsync
wäre lächerlich, zum Beispiel, da es kein Grund überhaupt für die asynchron zu sein. Anstattasync
Ihre Bewerbungawait
durchzuziehen , würden Sie am Ende überall propagieren .Dies würde auch das aktuelle Paradigma vollständig brechen und Probleme mit Eigenschaften verursachen (die praktisch nur Methodenpaare sind. Würden sie auch asynchron werden?) Und andere Auswirkungen auf das Design des Frameworks und der Sprache haben.
Wenn Sie viel IO-gebundene Arbeit erledigen, werden Sie feststellen, dass die
async
allgegenwärtige Verwendung eine großartige Ergänzung ist, viele Ihrer Routinen werden es seinasync
. Wenn Sie jedoch anfangen, CPU-gebundene Arbeit zu leisten, ist es im Allgemeinenasync
nicht gut , Dinge zu machen - es verbirgt die Tatsache, dass Sie CPU-Zyklen unter einer API verwenden, die asynchron zu sein scheint , aber nicht unbedingt wirklich asynchron ist.quelle
await FooAsync()
einfacher alsFoo()
? Und anstatt eines kleinen Dominoeffekts haben Sie manchmal einen großen Dominoeffekt und nennen das eine Verbesserung?Abgesehen von der Leistung kann Asynchronität Produktivitätskosten verursachen. Auf dem Client (WinForms, WPF, Windows Phone) ist dies ein Segen für die Produktivität. Aber auf dem Server oder in anderen Szenarien ohne Benutzeroberfläche zahlen Sie Produktivität. Sie möchten dort sicherlich nicht standardmäßig asynchron arbeiten. Verwenden Sie es, wenn Sie die Vorteile der Skalierbarkeit benötigen.
Verwenden Sie es, wenn Sie am Sweet Spot sind. In anderen Fällen nicht.
quelle
Ich glaube, es gibt einen guten Grund, alle Methoden asynchron zu machen, wenn sie nicht benötigt werden - Erweiterbarkeit. Selektives Erstellen von Methoden asynchron funktioniert nur, wenn sich Ihr Code nie weiterentwickelt und Sie wissen, dass Methode A () immer CPU-gebunden ist (Sie halten sie synchron) und Methode B () immer E / A-gebunden ist (Sie markieren sie als asynchron).
Aber was ist, wenn sich die Dinge ändern? Ja, A () führt Berechnungen durch, aber irgendwann in der Zukunft mussten Sie dort Protokollierung oder Berichterstellung oder benutzerdefinierten Rückruf mit nicht vorhersagbarer Implementierung hinzufügen, oder der Algorithmus wurde erweitert und umfasst jetzt nicht nur CPU-Berechnungen, sondern auch einige I / O? Sie müssen die Methode in asynchron konvertieren, dies würde jedoch die API beschädigen und alle Aufrufer im Stack müssten ebenfalls aktualisiert werden (und es können sogar verschiedene Apps von verschiedenen Anbietern sein). Oder Sie müssen neben der Synchronisierungsversion eine asynchrone Version hinzufügen, dies macht jedoch keinen großen Unterschied - die Verwendung der Synchronisierungsversion würde blockieren und ist daher kaum akzeptabel.
Es wäre großartig, wenn es möglich wäre, die vorhandene Synchronisierungsmethode asynchron zu machen, ohne die API zu ändern. In der Realität haben wir jedoch keine solche Option, und die Verwendung der asynchronen Version, auch wenn sie derzeit nicht benötigt wird, ist die einzige Möglichkeit, um sicherzustellen, dass Sie in Zukunft niemals auf Kompatibilitätsprobleme stoßen.
quelle