Wie kann vermieden werden, dass das DRY-Prinzip verletzt wird, wenn Sie sowohl asynchrone als auch synchronisierte Codeversionen benötigen?

15

Ich arbeite an einem Projekt, das sowohl die asynchrone als auch die synchrone Version derselben Logik / Methode unterstützen muss. So muss ich zum Beispiel haben:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

Die Async- und Sync-Logik für diese Methoden ist in jeder Hinsicht identisch, außer dass eine asynchron ist und eine andere nicht. Gibt es einen legitimen Weg, um in einem solchen Szenario eine Verletzung des DRY-Prinzips zu vermeiden? Ich habe gesehen, dass Leute sagen, Sie könnten GetAwaiter (). GetResult () für eine asynchrone Methode verwenden und sie von Ihrer Synchronisierungsmethode aus aufrufen? Ist dieser Thread in allen Szenarien sicher? Gibt es einen anderen, besseren Weg, dies zu tun, oder bin ich gezwungen, die Logik zu duplizieren?

Marko
quelle
1
Können Sie etwas mehr darüber sagen, was Sie hier tun? Wie erreichen Sie insbesondere Asynchronität in Ihrer asynchronen Methode? Ist die Arbeits-CPU mit hoher Latenz oder E / A gebunden?
Eric Lippert
"Einer ist asynchron und ein anderer nicht" ist der Unterschied im Code der tatsächlichen Methode oder nur in der Schnittstelle? (Klar nur für die Schnittstelle können Sie return Task.FromResult(IsIt());)
Alexei Levenkov
Vermutlich weisen sowohl die synchrone als auch die asynchrone Version eine hohe Latenz auf. Unter welchen Umständen wird der Anrufer die synchrone Version verwenden und wie wird dieser Anrufer die Tatsache abmildern, dass der Angerufene Milliarden von Nanosekunden benötigen könnte? Interessiert es den Anrufer der synchronen Version nicht, dass er die Benutzeroberfläche hängt? Gibt es keine Benutzeroberfläche? Erzählen Sie uns mehr über das Szenario.
Eric Lippert
@EricLippert Ich habe ein spezielles Dummy-Beispiel hinzugefügt, um Ihnen ein Gefühl dafür zu geben, wie zwei Codebasen identisch sind, abgesehen von der Tatsache, dass eine asynchron ist. Sie können sich leicht ein Szenario vorstellen, in dem diese Methode viel komplexer ist und Zeilen und Codezeilen dupliziert werden müssen.
Marko
@AlexeiLevenkov Der Unterschied liegt in der Tat im Code. Ich habe einen Dummy-Code hinzugefügt, um ihn zu demonstrieren.
Marko

Antworten:

15

Sie haben in Ihrer Frage mehrere Fragen gestellt. Ich werde sie etwas anders aufschlüsseln als Sie. Aber lassen Sie mich zuerst die Frage direkt beantworten.

Wir alle wollen eine Kamera, die leicht, hochwertig und billig ist, aber wie das Sprichwort sagt, können Sie höchstens zwei von diesen drei bekommen. Sie sind hier in der gleichen Situation. Sie möchten eine Lösung, die effizient und sicher ist und Code zwischen synchronen und asynchronen Pfaden teilt. Sie werden nur zwei davon bekommen.

Lassen Sie mich zusammenfassen, warum das so ist. Wir beginnen mit dieser Frage:


Ich habe gesehen, dass Leute sagen, Sie könnten GetAwaiter().GetResult()eine asynchrone Methode verwenden und sie von Ihrer Synchronisierungsmethode aus aufrufen? Ist dieser Thread in allen Szenarien sicher?

Der Punkt dieser Frage lautet: "Kann ich die synchronen und asynchronen Pfade gemeinsam nutzen, indem der synchrone Pfad einfach synchron auf die asynchrone Version wartet?"

Lassen Sie mich in diesem Punkt ganz klar sein, denn es ist wichtig:

Sie sollten sofort aufhören, Ratschläge von diesen Menschen zu nehmen .

Das ist ein extrem schlechter Rat. Es ist sehr gefährlich, ein Ergebnis einer asynchronen Aufgabe synchron abzurufen, es sei denn, Sie haben Beweise dafür, dass die Aufgabe normal oder abnormal abgeschlossen wurde .

Der Grund, warum dies ein extrem schlechter Rat ist, ist, dieses Szenario zu betrachten. Sie möchten den Rasen mähen, aber Ihr Rasenmähermesser ist gebrochen. Sie entscheiden sich für diesen Workflow:

  • Bestellen Sie eine neue Klinge von einer Website. Dies ist eine asynchrone Operation mit hoher Latenz.
  • Warten Sie synchron - das heißt, schlafen Sie , bis Sie die Klinge in der Hand haben .
  • Überprüfen Sie regelmäßig das Postfach, um festzustellen, ob das Blade angekommen ist.
  • Entfernen Sie die Klinge aus der Box. Jetzt haben Sie es in der Hand.
  • Installieren Sie die Klinge im Mäher.
  • Den Rasen mähen.

Was geschieht? Sie schlafen für immer, weil das Überprüfen der E-Mails jetzt auf etwas beschränkt ist, das nach dem Eintreffen der E-Mails geschieht .

Es ist sehr einfach , in diese Situation zu geraten, wenn Sie synchron auf eine beliebige Aufgabe warten. Für diese Aufgabe ist möglicherweise die Arbeit des Threads geplant, der jetzt wartet , und jetzt wird diese Zukunft niemals eintreffen, weil Sie darauf warten.

Wenn Sie asynchron warten, ist alles in Ordnung! Sie überprüfen regelmäßig die Post und während Sie warten, machen Sie ein Sandwich oder machen Ihre Steuern oder was auch immer; Sie erledigen Ihre Arbeit, während Sie warten.

Warten Sie niemals synchron. Wenn die Aufgabe erledigt ist, ist sie nicht erforderlich . Wenn die Aufgabe nicht erledigt ist, aber geplant ist, dass sie vom aktuellen Thread ausgeführt wird, ist sie ineffizient, da der aktuelle Thread möglicherweise andere Arbeiten erledigt, anstatt zu warten. Wenn die Aufgabe nicht erledigt ist und die Ausführung des aktuellen Threads geplant ist, bleibt sie hängen, um synchron zu warten. Es gibt keinen guten Grund, erneut synchron zu warten, es sei denn, Sie wissen bereits, dass die Aufgabe abgeschlossen ist .

Weitere Informationen zu diesem Thema finden Sie unter

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen erklärt das reale Szenario viel besser als ich.


Betrachten wir nun die "andere Richtung". Können wir Code gemeinsam nutzen, indem die asynchrone Version einfach die synchrone Version in einem Arbeitsthread ausführt?

Das ist aus den folgenden Gründen möglicherweise und in der Tat wahrscheinlich eine schlechte Idee.

  • Es ist ineffizient, wenn der synchrone Betrieb E / A-Arbeit mit hoher Latenz ist. Dies stellt im Wesentlichen einen Arbeiter ein und lässt diesen schlafen, bis eine Aufgabe erledigt ist. Fäden sind wahnsinnig teuer . Sie verbrauchen standardmäßig mindestens eine Million Byte Adressraum, benötigen Zeit und Betriebssystemressourcen. Sie möchten keinen Thread brennen, der nutzlose Arbeit leistet.

  • Die synchrone Operation ist möglicherweise nicht threadsicher geschrieben.

  • Dies ist eine vernünftigere Technik, wenn die Arbeit mit hoher Latenz prozessorgebunden ist, aber wenn dies der Fall ist, möchten Sie sie wahrscheinlich nicht einfach an einen Arbeitsthread übergeben. Sie möchten wahrscheinlich die Task-Parallelbibliothek verwenden, um sie auf so viele CPUs wie möglich zu parallelisieren, Sie möchten wahrscheinlich eine Abbruchlogik und Sie können die synchrone Version nicht einfach dazu bringen, all das zu tun, da es sich dann bereits um die asynchrone Version handelt .

Weiterführende Literatur; wieder erklärt Stephen es sehr deutlich:

Warum nicht Task.Run verwenden:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Weitere "Do and Don't" -Szenarien für Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


Was lässt uns das dann? Beide Techniken zum Teilen von Code führen entweder zu Deadlocks oder zu großen Ineffizienzen. Wir kommen zu dem Schluss, dass Sie eine Wahl treffen müssen. Möchten Sie ein Programm, das effizient und korrekt ist und den Anrufer begeistert, oder möchten Sie einige Tastenanschläge speichern, die durch das Duplizieren einer kleinen Codemenge zwischen dem synchronen und dem asynchronen Pfad entstehen? Ich fürchte, Sie bekommen nicht beides.

Eric Lippert
quelle
Können Sie erklären, warum die Rückkehr Task.Run(() => SynchronousMethod())in die asynchrone Version nicht das ist, was das OP will?
Guillaume Sasdy
2
@ GuillaumeSasdy: Das Originalplakat hat die Frage beantwortet, was die Arbeit ist: Es ist IO-gebunden. Es ist verschwenderisch, E / A-gebundene Aufgaben in einem Worker-Thread auszuführen!
Eric Lippert
Okay, ich verstehe. Ich denke du hast recht und auch die Antwort von @StriplingWarrior erklärt es noch mehr.
Guillaume Sasdy
2
@Marko Fast jede Problemumgehung, die den Thread blockiert, verbraucht schließlich (unter hoher Last) alle Threadpool-Threads (die normalerweise zum Abschließen von asynchronen Vorgängen verwendet werden). Infolgedessen warten alle Threads und es ist keiner verfügbar, damit Operationen einen Teil des Codes ausführen können, bei dem die asynchrone Operation abgeschlossen ist. Die meisten Problemumgehungen sind in Szenarien mit geringer Last in Ordnung (da viele Threads vorhanden sind, werden sogar 2-3 als Teil jeder einzelnen Operation blockiert). Und wenn Sie sicherstellen können, dass der Abschluss der asynchronen Operation auf einem neuen Betriebssystem-Thread (nicht einem Thread-Pool) ausgeführt wird kann sogar für alle Fälle funktionieren (Sie zahlen also einen hohen Preis)
Alexei Levenkov
2
Manchmal gibt es wirklich keinen anderen Weg als "einfach die synchrone Version in einem Worker-Thread ausführen". So wird es beispielsweise Dns.GetHostEntryAsyncin .NET und FileStream.ReadAsyncfür bestimmte Dateitypen implementiert . Das Betriebssystem bietet einfach keine Asynchron - Schnittstelle, so dass die Laufzeit zu fälschen hat (und es ist nicht die Sprache spezifisch sagen wir, Erlang Laufzeit läuft gesamten Baum der Worker - Prozesse , mit mehreren Threads innerhalb eines jeden, nicht blockierende Platten - I / O und den Namen zur Verfügung zu stellen Auflösung).
Joker_vD
6

Es ist schwierig, eine einheitliche Antwort auf diese Frage zu geben. Leider gibt es keine einfache und perfekte Möglichkeit, zwischen asynchronem und synchronem Code wiederzuverwenden. Aber hier sind einige Prinzipien zu beachten:

  1. Asynchroner und synchroner Code unterscheiden sich häufig grundlegend. Asynchroner Code sollte normalerweise beispielsweise ein Storno-Token enthalten. Und oft werden verschiedene Methoden aufgerufen (wie in Ihrem Beispiel die Query()eine und QueryAsync()die andere aufgerufen) oder Verbindungen mit unterschiedlichen Einstellungen hergestellt. Selbst wenn es strukturell ähnlich ist, gibt es oft genug Verhaltensunterschiede, um als separater Code mit unterschiedlichen Anforderungen behandelt zu werden. Beachten Sie die Unterschiede zwischen Async- und Sync- Implementierungen von Methoden in der File-Klasse, zum Beispiel: Es werden keine Anstrengungen unternommen, um sie dazu zu bringen, denselben Code zu verwenden
  2. Wenn Sie eine asynchrone Methodensignatur für die Implementierung einer Schnittstelle bereitstellen, aber zufällig eine synchrone Implementierung haben (dh die Funktionsweise Ihrer Methode ist von Natur aus nicht asynchron), können Sie einfach zurückkehren Task.FromResult(...).
  3. Jegliche Synchronstück Logik , welche sind das gleiche zwischen den beiden Verfahren können in beiden Verfahren zu einer separaten Hilfsmethode und gehebelt extrahiert werden.

Viel Glück.

StriplingWarrior
quelle
-2

Einfach; Lassen Sie den Synchronen den Asynchronen aufrufen. Es gibt sogar eine praktische Methode Task<T>, um genau das zu tun:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}
KeithS
quelle
1
Siehe meine Antwort, warum dies eine schlechteste Praxis ist, die Sie niemals tun dürfen.
Eric Lippert
1
Ihre Antwort bezieht sich auf GetAwaiter (). GetResult (). Das habe ich vermieden. Die Realität ist, dass manchmal eine Methode synchron ausgeführt werden muss, um sie in einem Aufrufstapel zu verwenden, der nicht asynchronisiert werden kann. Wenn Synchronisierungswartezeiten niemals durchgeführt werden sollten, teilen Sie mir als (ehemaliges) Mitglied des C # -Teams bitte mit, warum Sie nicht nur eine, sondern zwei Möglichkeiten zum synchronen Warten auf eine asynchrone Aufgabe in die TPL aufgenommen haben.
KeithS
Die Person, die diese Frage stellt, ist Stephen Toub, nicht ich. Ich habe nur auf der sprachlichen Seite der Dinge gearbeitet.
Eric Lippert