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.
Wenn mein gesamter Code langsam asynchron wird, warum nicht einfach standardmäßig alles asynchron machen?
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.
Wenn die Leistung das einzige Problem ist, können sicherlich einige clevere Optimierungen den Overhead automatisch entfernen, wenn er nicht benötigt wird.
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 wie x+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:
M();
N();
und M()
war void M() { Q(); R(); }
, N()
war void N() { S(); T(); }
und R
und S
erzeugt Nebenwirkungen, dann wissen Sie, dass die Nebenwirkung von R vor der Nebenwirkung von S auftritt. Aber wenn Sie async void M() { await Q(); R(); }
dann plötzlich haben, geht das aus dem Fenster. Sie haben keine Garantie dafür, ob R()
dies vorher oder nachher geschehen wird S()
(es sei denn, es wird natürlich M()
erwartet; aber natürlich Task
muss es erst danach erwartet werden N()
.)
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 nicht Lazy<T>
?" Zu beantworten. oder "warum nichtNullable<T>
?" oder "warum nicht IEnumerable<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.