Diese Frage betrifft die C # -Sprache, aber ich erwarte, dass sie andere Sprachen wie Java oder TypeScript abdeckt.
Microsoft empfiehlt Best Practices für die Verwendung asynchroner Aufrufe in .NET. Wählen wir unter diesen Empfehlungen zwei aus:
- Ändern Sie die Signatur der asynchronen Methoden so, dass sie Task oder Task <> zurückgeben (in TypeScript wäre das ein Versprechen <>).
- ändere die Namen der asynchronen Methoden so, dass sie mit xxxAsync () enden
Wenn Sie nun eine synchrone Komponente auf niedriger Ebene durch eine asynchrone Komponente ersetzen, wirkt sich dies auf den gesamten Stapel der Anwendung aus. Da sich async / await nur dann positiv auswirkt, wenn es "ganz nach oben" verwendet wird, müssen die Signatur und die Methodennamen aller Ebenen in der Anwendung geändert werden.
Eine gute Architektur beinhaltet häufig das Platzieren von Abstraktionen zwischen den einzelnen Ebenen, sodass das Ersetzen von Komponenten auf niedriger Ebene durch andere von den Komponenten auf höherer Ebene nicht gesehen wird. In C # haben Abstraktionen die Form von Schnittstellen. Wenn wir eine neue asynchrone Komponente auf niedriger Ebene einführen, muss jede Schnittstelle im Aufrufstapel entweder geändert oder durch eine neue Schnittstelle ersetzt werden. Die Art und Weise, wie ein Problem in einer implementierenden Klasse gelöst wird (asynchron oder synchron), ist für die Aufrufer nicht mehr verborgen (abstrahiert). Die Anrufer müssen wissen, ob es synchron oder asynchron ist.
Sind nicht asynchrone / erwartete Best Practices, die im Widerspruch zu den Prinzipien der "guten Architektur" stehen?
Bedeutet dies, dass jede Schnittstelle (z. B. IEnumerable, IDataAccessLayer) ihr asynchrones Gegenstück (IAsyncEnumerable, IAsyncDataAccessLayer) benötigt, damit sie beim Wechsel zu asynchronen Abhängigkeiten im Stapel ersetzt werden können?
Wenn wir das Problem ein wenig weiter schieben, wäre es nicht einfacher anzunehmen, dass jede Methode asynchron ist (um eine Aufgabe <> oder eine Zusage <> zurückzugeben), und dass die Methoden die asynchronen Aufrufe synchronisieren, wenn sie nicht tatsächlich sind asynchron? Ist das von den zukünftigen Programmiersprachen zu erwarten?
quelle
CancellationToken
, und diejenigen, die dies tun, möchten möglicherweise eine Standardmethode angeben). Das Entfernen der vorhandenen Synchronisierungsmethoden (und das proaktive Brechen des gesamten Codes) ist ein offensichtlicher Nichtstarter.Antworten:
Welche Farbe hat deine Funktion?
Sie könnten an Bob Nystroms What Color Is Your Function 1 interessiert sein .
In diesem Artikel beschreibt er eine fiktive Sprache, in der:
Dies ist zwar fiktiv, geschieht jedoch in folgenden Programmiersprachen regelmäßig:
this
.Wie Sie erkannt haben, neigen rote Funktionen aufgrund dieser Regeln dazu, sich in der Codebasis zu verbreiten . Sie fügen eine ein und nach und nach kolonisiert sie die gesamte Codebasis.
1 Bob Nystrom ist neben dem Bloggen auch Teil des Dart-Teams und hat diese kleine Crafting Interpreters-Serie geschrieben. Sehr empfehlenswert für alle Programmiersprachen / Compiler-Fans.
2 Nicht ganz richtig, da Sie möglicherweise eine asynchrone Funktion aufrufen und blockieren, bis sie zurückkehrt, aber ...
Spracheinschränkung
Dies ist im Wesentlichen eine Sprach- / Laufzeitbeschränkung.
Eine Sprache mit M: N-Threading, z. B. Erlang und Go, hat keine
async
Funktionen: Jede Funktion ist möglicherweise asynchron, und ihre "Faser" wird einfach angehalten, ausgetauscht und wieder eingelesen, wenn sie wieder bereit ist.C # entschied sich für ein 1: 1-Threading-Modell und daher für die Oberflächensynchronität in der Sprache, um ein versehentliches Blockieren von Threads zu vermeiden.
Bei Spracheinschränkungen müssen die Kodierungsrichtlinien angepasst werden.
quelle
async
); In der Tat war es ein ziemlich verbreitetes Muster für die Erstellung einer reaktionsfähigen Benutzeroberfläche. War es so hübsch wie Erlang? Nee. Aber das ist noch weit entfernt von C :)Sie haben Recht, es gibt hier einen Widerspruch, aber es ist nicht die "beste Vorgehensweise", schlecht zu sein. Dies liegt daran, dass eine asynchrone Funktion wesentlich andere Funktionen ausführt als eine synchrone. Anstatt auf das Ergebnis seiner Abhängigkeiten zu warten (normalerweise einige E / A-Vorgänge), wird eine Aufgabe erstellt, die von der Hauptereignisschleife behandelt wird. Dies ist kein Unterschied, der sich unter Abstraktion gut verbergen lässt.
quelle
async
Methoden in C # verwenden, um auch synchrone Verträge bereitzustellen, wenn Sie möchten. Der einzige Unterschied besteht darin, dass Go standardmäßig asynchron ist, während C # standardmäßig synchron ist.async
gibt Ihnen das zweite Programmiermodell -async
ist die Abstraktion (was es tatsächlich tut, hängt von der Laufzeit, dem Task-Scheduler, dem Synchronisationskontext, der Waiter-Implementierung ... ab).Wie Sie sicher wissen, verhält sich eine asynchrone Methode anders als eine synchrone. Zur Laufzeit ist die Umwandlung eines asynchronen in einen synchronen Aufruf trivial, aber das Gegenteil kann nicht gesagt werden. Also wird die Logik dann, warum machen wir nicht asynchrone Methoden von jeder Methode, die es erfordern könnte, und lassen den Aufrufer nach Bedarf in eine synchrone Methode "konvertieren"?
In gewissem Sinne ist es so, als hätte man eine Methode, die Ausnahmen auslöst, und eine andere, die "sicher" ist und selbst im Fehlerfall keine Ausnahmen macht. Ab wann ist der Codierer übermäßig, um diese Methoden bereitzustellen, die andernfalls in andere konvertiert werden können?
Dabei gibt es zwei Denkrichtungen: Eine besteht darin, mehrere Methoden zu erstellen, von denen jede eine andere, möglicherweise private Methode, aufruft und die Möglichkeit bietet, optionale Parameter oder geringfügige Änderungen des Verhaltens wie Asynchronität bereitzustellen. Die andere besteht darin, die Schnittstellenmethoden auf das Nötigste zu beschränken und es dem Aufrufer zu überlassen, die erforderlichen Änderungen selbst vorzunehmen.
Wenn Sie zur ersten Schule gehören, gibt es eine gewisse Logik, eine Klasse für synchrone und asynchrone Anrufe zu reservieren, um zu vermeiden, dass sich jeder Anruf verdoppelt. Microsoft tendiert dazu, diese Denkweise zu bevorzugen, und um dem von Microsoft favorisierten Stil zu entsprechen, müssten auch Sie eine Async-Version haben, ähnlich wie Schnittstellen fast immer mit einem "I" beginnen. Lassen Sie mich betonen, dass es per se nicht falsch ist, weil es besser ist, einen konsistenten Stil in einem Projekt beizubehalten, als ihn "richtig" zu machen und den Stil für die Entwicklung, die Sie einem Projekt hinzufügen, radikal zu ändern.
Trotzdem bevorzuge ich die zweite Schule, um die Schnittstellenmethoden zu minimieren. Wenn ich denke, dass eine Methode asynchron aufgerufen werden kann, ist die Methode für mich asynchron. Der Anrufer kann entscheiden, ob er auf die Beendigung dieser Aufgabe warten möchte oder nicht, bevor er fortfährt. Wenn es sich bei dieser Schnittstelle um eine Schnittstelle zu einer Bibliothek handelt, ist dies sinnvoller, um die Anzahl der Methoden zu minimieren, die Sie veralten oder anpassen müssen. Wenn die Schnittstelle für die interne Verwendung in meinem Projekt vorgesehen ist, füge ich für jeden erforderlichen Aufruf im gesamten Projekt eine Methode für die angegebenen Parameter und keine "zusätzlichen" Methoden hinzu, und zwar nur dann, wenn das Verhalten der Methode noch nicht abgedeckt ist durch eine bestehende Methode.
Wie viele andere Dinge in diesem Bereich ist es jedoch weitgehend subjektiv. Beide Ansätze haben Vor- und Nachteile. Microsoft hat außerdem die Konvention eingeführt, am Anfang des Variablennamens Buchstaben einzufügen, die den Typ anzeigen, und "m_", um anzuzeigen, dass es sich um ein Mitglied handelt, was zu Variablennamen wie führt
m_pUser
. Mein Punkt ist, dass nicht einmal Microsoft unfehlbar ist und auch Fehler machen kann.Das heißt, wenn Ihr Projekt dieser Async-Konvention folgt, rate ich Ihnen, diese zu respektieren und den Stil fortzusetzen. Und erst wenn Sie ein eigenes Projekt erhalten haben, können Sie es so schreiben, wie Sie es für richtig halten.
quelle
.Wait()
method and like negative Konsequenzen haben, und soweit ich weiß, ist dies in js überhaupt nicht möglich.Promise.resolve(x)
diese Rückrufe nicht sofort ausgeführt , selbst wenn Sie sie aufrufen und anschließend hinzufügen.Stellen wir uns vor, es gibt eine Möglichkeit, Funktionen asynchron aufzurufen, ohne ihre Signatur zu ändern.
Das wäre wirklich cool und niemand würde empfehlen, den Namen zu ändern.
Tatsächliche asynchrone Funktionen, nicht nur solche, die auf eine andere asynchrone Funktion warten, sondern auch die unterste Ebene, weisen eine Struktur auf, die für ihre asynchrone Natur spezifisch ist. z.B
Es sind diese beiden Informationen, die der aufrufende Code benötigt, um den Code auf eine asynchrone Art
Task
und Weise auszuführen, die Dinge mögen undasync
kapseln.Es gibt mehr als eine Möglichkeit, asynchrone Funktionen auszuführen. Es
async Task
gibt nur ein Muster, das über Rückgabetypen in den Compiler integriert wird, damit Sie die Ereignisse nicht manuell verknüpfen müssenquelle
Ich werde den Hauptpunkt auf eine weniger kultivierte und allgemeinere Weise ansprechen:
Ich würde sagen, dass es nur von der Wahl abhängt, die Sie im Design Ihrer API treffen und was Sie dem Benutzer überlassen.
Wenn eine Funktion Ihrer API nur asynchron sein soll, besteht wenig Interesse daran, die Namenskonvention einzuhalten. Geben Sie einfach immer Task <> / Promise <> / Future <> / ... als Rückgabetyp zurück, es ist selbstdokumentierend. Wenn er eine Synchronisierungsantwort haben möchte, kann er das immer noch durch Warten tun, aber wenn er das immer tut, macht es ein bisschen Boilerplate.
Wenn Sie jedoch nur Ihre API-Synchronisierung durchführen, bedeutet dies, dass ein Benutzer den asynchronen Teil selbst verwalten muss, wenn er möchte, dass er asynchron ist.
Dies kann eine Menge zusätzlicher Arbeit bedeuten, aber es kann dem Benutzer auch mehr Kontrolle darüber geben, wie viele gleichzeitige Anrufe er zulässt, eine Zeitüberschreitung einleitet, erneut versucht und so weiter.
In einem großen System mit einer großen API ist es möglicherweise einfacher und effizienter, die meisten von ihnen standardmäßig für die Synchronisierung zu implementieren, als jeden Teil Ihrer API unabhängig zu verwalten, insbesondere wenn sie Ressourcen (Dateisystem, CPU, Datenbank, ...) gemeinsam nutzen.
Tatsächlich könnten Sie für die komplexesten Teile perfekt zwei Implementierungen desselben Teils Ihrer API haben, eine synchrone, die die praktischen Dinge erledigt, eine asynchrone, die sich auf die synchronen Dinge verlässt und nur Parallelität, Ladevorgänge, Zeitüberschreitungen und Wiederholungsversuche verwaltet .
Vielleicht kann jemand anderes seine Erfahrungen damit teilen, weil mir Erfahrungen mit solchen Systemen fehlen.
quelle