it-swarm.com.de

Widersprechen die Richtlinien für die Verwendung von Async / Warten in C # nicht den Konzepten guter Architektur und Abstraktionsschichtung?

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 <>).
  • ändern Sie die Namen der asynchronen Methoden so, dass sie mit xxxAsync () enden.

Wenn Sie nun eine synchrone Komponente auf niedriger Ebene durch eine asynchrone ersetzen, wirkt sich dies auf den gesamten Stapel der Anwendung aus. Da sich async/await nur dann positiv auswirkt, wenn es "ganz oben" verwendet wird, müssen die Signatur- und Methodennamen aller Ebenen in der Anwendung geändert werden.

Bei einer guten Architektur werden häufig Abstraktionen zwischen den einzelnen Ebenen platziert, 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 (asynchron oder synchronisiert) wird, ist für die Aufrufer nicht mehr verborgen (abstrahiert). Die Anrufer müssen wissen, ob es synchron oder asynchron ist.

Sind asynchrone/erwartete Best Practices nicht im Widerspruch zu den Prinzipien der "guten Architektur"?

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 etwas weiter vorantreiben, wäre es nicht einfacher anzunehmen, dass jede Methode asynchron ist (um eine Aufgabe <> oder ein Versprechen <> zurückzugeben) und dass die Methoden die asynchronen Aufrufe synchronisieren, wenn sie nicht tatsächlich sind asynchron? Ist dies von den zukünftigen Programmiersprachen zu erwarten?

106
corentinaltepe

Welche Farbe hat Ihre Funktion?

Sie könnten an Bob Nystroms Welche Farbe ist Ihre Funktion interessiert sein1.

In diesem Artikel beschreibt er eine fiktive Sprache, in der:

  • Jede Funktion hat eine Farbe: blau oder rot.
  • Eine rote Funktion kann entweder blaue oder rote Funktionen aufrufen, kein Problem.
  • Eine blaue Funktion kann nur blaue Funktionen aufrufen.

Obwohl fiktiv, geschieht dies ziemlich regelmäßig in Programmiersprachen:

  • In C++ kann eine "const" -Methode nur andere "const" -Methoden für this aufrufen.
  • In Haskell kann eine Nicht-E/A-Funktion nur Nicht-E/A-Funktionen aufrufen.
  • In C # kann eine Synchronisierungsfunktion nur Synchronisierungsfunktionen aufrufen2.

Wie Sie festgestellt haben, neigen rote Funktionen aufgrund dieser Regeln dazu, zu verbreiten Um die Codebasis herum. 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 eine asynchrone Funktion aufrufen und blockieren können, bis sie zurückkehrt, aber ...

Sprachbeschränkung

Dies ist im Wesentlichen eine Sprach-/Laufzeitbeschränkung.

Eine Sprache mit M: N-Threading wie Erlang und Go verfügt beispielsweise nicht über async -Funktionen: Jede Funktion ist möglicherweise asynchron, und ihre "Faser" wird einfach angehalten, ausgetauscht und wieder eingetauscht, wenn es ist wieder fertig.

C # entschied sich für ein 1: 1-Threading-Modell und entschied sich daher für die Oberflächensynchronität in der Sprache, um ein versehentliches Blockieren von Threads zu vermeiden.

Bei Sprachbeschränkungen müssen sich die Codierungsrichtlinien anpassen.

110
Matthieu M.

Sie haben Recht, hier gibt es einen Widerspruch, aber es sind nicht die "Best Practices", die schlecht sind. Dies liegt daran, dass die asynchrone Funktion wesentlich anders ist als eine synchrone. Anstatt auf das Ergebnis aus seinen Abhängigkeiten zu warten (normalerweise einige E/A), wird eine Aufgabe erstellt, die von der Hauptereignisschleife verarbeitet wird. Dies ist kein Unterschied, der unter Abstraktion gut verborgen werden kann.

82
max630

Eine asynchrone Methode verhält sich anders als eine synchrone, wie Sie sicher wissen. Zur Laufzeit ist es trivial, einen asynchronen Aufruf in einen synchronen umzuwandeln, aber das Gegenteil kann nicht gesagt werden. Daher wird die Logik dann, warum machen wir nicht asynchrone Methoden für jede Methode, die dies 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 nicht auslöst. Ab wann ist der Codierer übermäßig, um diese Methoden bereitzustellen, die ansonsten in eine 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, wodurch optionale Parameter oder geringfügige Änderungen des Verhaltens wie z. B. Asynchronität bereitgestellt werden können. Die andere besteht darin, die Schnittstellenmethoden so gering wie möglich zu halten, damit das Wesentliche dem Anrufer überlassen bleibt, die erforderlichen Änderungen selbst vorzunehmen.

Wenn Sie der ersten Schule angehören, gibt es eine bestimmte Logik, eine Klasse synchronen und asynchronen Anrufen zu widmen, um zu vermeiden, dass sich jeder Anruf verdoppelt. Microsoft tendiert dazu, diese Denkrichtung zu bevorzugen, und um im Einklang mit dem von Microsoft favorisierten Stil zu bleiben, 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 es "richtig" zu machen und den Stil für die von Ihnen entwickelte Entwicklung radikal zu ändern zu einem Projekt hinzufügen.

Trotzdem tendiere ich dazu, die zweite Schule zu bevorzugen, die darin besteht, 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 warten soll, bis diese Aufgabe abgeschlossen ist, bevor er fortfährt. Wenn diese Schnittstelle eine Schnittstelle zu einer Bibliothek ist, ist es sinnvoller, dies auf diese Weise zu tun, um die Anzahl der Methoden zu minimieren, die Sie verwerfen oder anpassen müssen. Wenn die Schnittstelle für den internen Gebrauch in meinem Projekt vorgesehen ist, füge ich für jeden erforderlichen Aufruf in meinem Projekt eine Methode für die angegebenen Parameter und keine "zusätzlichen" Methoden hinzu, und selbst dann nur, wenn das Verhalten der Methode noch nicht behandelt wurde durch eine bestehende Methode.

Wie viele Dinge auf diesem Gebiet ist es jedoch weitgehend subjektiv. Beide Ansätze haben ihre Vor- und Nachteile. Microsoft hat außerdem die Konvention eingeführt, Buchstaben am Anfang des Variablennamens und "m_" hinzuzufügen, um anzuzeigen, dass es sich um ein Mitglied handelt, was zu Variablennamen wie m_pUser Führt. Mein Punkt ist, dass nicht einmal Microsoft unfehlbar ist und auch Fehler machen kann.

Wenn Ihr Projekt jedoch dieser Async-Konvention folgt, würde ich Ihnen raten, 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.

6
Neil

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, dass Sie ihre Namen ändern.

Tatsächliche asynchrone Funktionen, nicht nur solche, die auf eine andere asynchrone Funktion warten, sondern die unterste Ebene haben eine Struktur, die spezifisch für ihre asynchrone Natur ist. z.B

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Es sind diese beiden Informationen, die der aufrufende Code benötigt, um den Code asynchron auszuführen, die Dinge wie Task und async kapseln.

Es gibt mehr als eine Möglichkeit, asynchrone Funktionen auszuführen: async Task ist nur ein Muster, das über Rückgabetypen in den Compiler integriert ist, sodass Sie die Ereignisse nicht manuell verknüpfen müssen

2
Ewan

Ich werde den Hauptpunkt weniger C # ness und allgemeiner ansprechen:

Sind asynchrone/erwartete Best Practices nicht im Widerspruch zu den Prinzipien der "guten Architektur"?

Ich würde sagen, dass es nur von der Wahl abhängt, die Sie beim Design Ihrer API treffen, und davon, was Sie dem Benutzer überlassen.

Wenn eine Funktion Ihrer API nur asynchron sein soll, besteht wenig Interesse daran, die Namenskonvention zu befolgen. Geben Sie einfach immer Task <>/Promise <>/Future <>/... als Rückgabetyp zurück, es ist selbstdokumentierend. Wenn er eine Synchronisierungsantwort wünscht, kann er das immer noch tun, indem er wartet, aber wenn er das immer tut, macht es ein bisschen Boilerplate.

Wenn Sie jedoch nur Ihre API synchronisieren, 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, kann dem Benutzer jedoch auch mehr Kontrolle darüber geben, wie viele gleichzeitige Anrufe er zulässt, Zeitüberschreitungen platziert, Wiederholungen versucht usw.

In einem großen System mit einer großen API ist es möglicherweise einfacher und effizienter, die meisten von ihnen standardmäßig zu synchronisieren, 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 zwei Implementierungen desselben Teils Ihrer API haben, eine synchron, die die praktischen Aufgaben erledigt, eine asynchrone, die sich auf die synchronen Aufgaben stützt und nur Parallelität, Ladevorgänge, Zeitüberschreitungen und Wiederholungsversuche verwaltet .

Vielleicht kann jemand anderes seine Erfahrungen damit teilen, weil mir Erfahrung mit solchen Systemen fehlt.

0
Walfrat