Warum benötigen wir das Schlüsselwort async?

19

Ich habe gerade angefangen, mit async / await in .Net 4.5 herumzuspielen. Ich bin zunächst gespannt, warum das asynchrone Schlüsselwort erforderlich ist. Die Erklärung, die ich gelesen habe, war, dass es ein Marker ist, damit der Compiler weiß, dass eine Methode auf etwas wartet. Es scheint jedoch, dass der Compiler in der Lage sein sollte, dies ohne ein Schlüsselwort herauszufinden. Was macht es sonst noch?

ConditionRacer
quelle
2
Hier ist ein wirklich guter Blog-Artikel (Serie), der das Schlüsselwort in C # und die zugehörigen Funktionen in F # ausführlich beschreibt.
Paul
Ich denke, es ist hauptsächlich ein Marker für den Entwickler und nicht für den Compiler.
CodesInChaos
8
Dieser Artikel erklärt es: blogs.msdn.com/b/ericlippert/archive/2010/11/11/…
svick
@svick danke, das ist was ich gesucht habe. Macht jetzt vollkommen Sinn.
ConditionRacer

Antworten:

23

Hier gibt es mehrere Antworten, und alle sprechen darüber, was asynchrone Methoden tun, aber keine von ihnen beantwortet die Frage, weshalb asyncsie als Schlüsselwort für die Funktionsdeklaration benötigt werden.

Es geht nicht darum, "den Compiler anzuweisen, die Funktion auf besondere Weise zu transformieren". awaitallein könnte das tun. Warum? Da C # hat bereits einen anderen Mechanismus , bei dem die Anwesenheit eines speziellen Schlüsselwort in dem Verfahren Körper des Compilers verursacht extreme auszuführen (und sehr ähnlich async/await) Transformationen auf dem Verfahren Körper: yield.

Nur dass dies yieldkein eigenes Schlüsselwort in C # ist und das Verständnis, warum dies so ist, asyncauch erklärt. Anders als in den meisten Sprachen, die diesen Mechanismus unterstützen, können Sie in C # nicht sagen, dass yield value;Sie yield return value;stattdessen sagen müssen . Warum? Weil es der Sprache hinzugefügt wurde, nachdem C # bereits vorhanden war, und es durchaus vernünftig war anzunehmen, dass jemand irgendwo yieldden Namen einer Variablen verwendet haben könnte. Da es jedoch kein bereits vorhandenes Szenario gab, das <variable name> returnsyntaktisch korrekt war, wurde yield returndie Sprache erweitert, um die Einführung von Generatoren zu ermöglichen und gleichzeitig die 100% ige Abwärtskompatibilität mit vorhandenem Code zu gewährleisten.

Aus diesem Grund asyncwurde es als Funktionsmodifikator hinzugefügt, um zu vermeiden, dass vorhandener Code, der awaitals Variablenname verwendet wird, beschädigt wird. Da bereits keine asyncMethoden vorhanden waren, wird kein alter Code ungültig gemacht. In neuem Code kann der Compiler anhand des vorhandenen asyncTags feststellen , dass awaitdieses als Schlüsselwort und nicht als Bezeichner behandelt werden soll.

Mason Wheeler
quelle
3
Dies ist so ziemlich das, was dieser Artikel auch sagt (ursprünglich gepostet von @svick in den Kommentaren), aber niemand hat es jemals als Antwort gepostet. Hat nur zweieinhalb Jahre gedauert! Danke :)
ConditionRacer
Bedeutet das nicht, dass in einigen zukünftigen Versionen die Anforderung zur Angabe des asyncSchlüsselworts entfallen kann? Natürlich wäre es eine BC-Unterbrechung, aber es könnte davon ausgegangen werden, dass dies nur sehr wenige Projekte betrifft und eine direkte Voraussetzung für ein Upgrade ist (dh das Ändern eines Variablennamens).
Quolonel Fragen
6

Es ändert die Methode von einer normalen Methode in ein Objekt mit Rückruf, was einen völlig anderen Ansatz für die Codegenerierung erfordert

und wenn so etwas drastisches passiert, ist es üblich, es klar zu kennzeichnen (wir haben diese Lektion aus C ++ gelernt)

Ratschenfreak
quelle
Das ist also wirklich etwas für den Leser / Programmierer und nicht so sehr für den Compiler?
ConditionRacer
@ Justin984 es hilft definitiv dem compiler zu signalisieren, dass etwas besonderes passieren muss
ratschenfreak
Ich denke, ich bin gespannt, warum der Compiler es braucht. Könnte es nicht einfach die Methode analysieren und sehen, dass das Warten irgendwo im Methodenkörper ist?
ConditionRacer
Es ist hilfreich, wenn Sie mehrere asyncs gleichzeitig planen möchten, damit sie nicht nacheinander, sondern gleichzeitig ausgeführt werden (wenn mindestens Threads verfügbar sind)
Ratschenfreak
@ Justin984: Nicht jede Methode, die zurückgibt, Task<T>weist tatsächlich die Merkmale einer asyncMethode auf. Sie könnten beispielsweise nur eine Geschäfts- / Fabriklogik durchlaufen und dann an eine andere Methode delegieren, die zurückgibt Task<T>. asyncMethoden haben einen Rückgabetyp von Task<T>in der Signatur, geben aber nicht tatsächlich a zurück Task<T>, sondern nur a T. Um all dies ohne das asyncSchlüsselwort herauszufinden , müsste der C # -Compiler alle möglichen Tiefenprüfungen der Methode durchführen, was sie wahrscheinlich erheblich verlangsamen und zu allen möglichen Unklarheiten führen würde.
Aaronaught
4

Die ganze Idee mit Schlüsselwörtern wie "async" oder "unsicher" besteht darin, Unklarheiten darüber zu beseitigen, wie der Code, den sie ändern, behandelt werden soll. Im Fall des Schlüsselworts async wird der Compiler angewiesen, die geänderte Methode als etwas zu behandeln, das nicht sofort zurückgegeben werden muss. Dies ermöglicht es dem Thread, in dem diese Methode verwendet wird, fortzufahren, ohne auf die Ergebnisse dieser Methode warten zu müssen. Es ist effektiv eine Code-Optimierung.

Weltingenieur
quelle
Ich bin versucht, abzulehnen, weil der Vergleich mit Interrupts zwar korrekt ist, aber nicht korrekt ist (nach meiner informierten Meinung).
Paul
Ich sehe sie in Bezug auf den Zweck als etwas analog an, aber ich werde sie aus Gründen der Klarheit entfernen.
Welt Ingenieur
Interrupts ähneln in meinen Augen eher Ereignissen als asynchronem Code - sie führen zu einem Kontextwechsel und werden vollständig ausgeführt, bevor der normale Code wieder aufgenommen wird (und der normale Code weiß möglicherweise auch nichts davon), wohingegen die Methode bei asynchronem Code möglicherweise nicht ausgeführt wird nicht abgeschlossen, bevor der aufrufende Thread fortgesetzt wird. Ich sehe, was Sie versucht haben, über verschiedene Mechanismen zu sagen, die unterschiedliche Implementierungen erfordern, aber Interrupts halfen mir einfach nicht, diesen Punkt zu veranschaulichen. (+1 für den Rest, übrigens)
Paul
0

OK, hier ist meine Einstellung dazu.

Es gibt so etwas wie Koroutinen , die seit Jahrzehnten bekannt sind. ("Knuth and Hopper" -Klasse "seit Jahrzehnten") Sie sind Verallgemeinerungen von Unterprogrammen , indem sie nicht nur die Kontrolle über den Funktionsstart und die Rückgabeanweisung erhalten und freigeben, sondern dies auch an bestimmten Punkten ( Suspendierungspunkten ). Eine Subroutine ist eine Coroutine ohne Aufhängepunkte.

Sie sind EINFACH mit C-Makros zu implementieren, wie im folgenden Artikel über "Protothreads" gezeigt. ( http://dunkels.com/adam/dunkels06protothreads.pdf ) Lesen Sie es. Ich werde warten...

Unterm Strich dafür ist , dass die Makros ein großen erstellen switch, und ein caseEtikett an jedem Aufhängungspunkt. An jedem Unterbrechungspunkt speichert die Funktion den Wert des unmittelbar folgenden caseEtiketts, damit sie weiß, wo die Ausführung beim nächsten Aufruf fortgesetzt werden soll. Und es gibt die Kontrolle an den Anrufer zurück.

Dies geschieht ohne Änderung des scheinbaren Kontrollflusses des im "Protothread" beschriebenen Codes.

Stellen Sie sich vor, Sie haben eine große Schleife, die alle diese "Protothreads" der Reihe nach aufruft und gleichzeitig "Protothreads" für einen einzelnen Thread ausführt.

Dieser Ansatz hat zwei Nachteile:

  1. Sie können den Status in lokalen Variablen zwischen den Wiederaufnahmen nicht beibehalten.
  2. Sie können den "Protothread" nicht von einer beliebigen Aufruftiefe aussetzen. (alle Aufhängepunkte müssen auf Level 0 sein)

Es gibt Problemumgehungen für beide:

  1. Alle lokalen Variablen müssen in den Kontext des Protothreads gezogen werden (Kontext, der bereits benötigt wird, weil der Protothread seinen nächsten Wiederaufnahmepunkt speichern muss)
  2. Wenn Sie der Meinung sind, dass Sie wirklich einen anderen Protothread von einem Protothread aufrufen müssen, "laichen" Sie einen untergeordneten Protothread und setzen Sie ihn aus, bis das untergeordnete Element fertig ist.

Und wenn Sie Compiler-Unterstützung hätten, um die Umschreibungsarbeit zu erledigen, die die Makros und die Problemumgehung leisten, könnten Sie einfach Ihren Protothread-Code so schreiben, wie Sie beabsichtigen, und Suspendierungspunkte mit einem Schlüsselwort einfügen.

Und genau darum geht es asyncund awaitdarum, (stapellose) Koroutinen zu erstellen.

Die Coroutinen in C # werden als Objekte der Klasse (generisch oder nicht generisch) geändert Task.

Ich finde diese Keywords sehr irreführend. Meine gedankliche Lektüre ist:

  • async als "suspensible"
  • await als "Aussetzen bis zum Abschluss von"
  • Task als "Zukunft ..."

Jetzt. Müssen wir die Funktion wirklich markieren async? Abgesehen davon, dass es die Mechanismen zum Umschreiben von Code auslösen sollte, um die Funktion zu einer Koroutine zu machen, werden einige Unklarheiten gelöst. Betrachten Sie diesen Code.

public Task<object> AmIACoroutine() {
    var tcs = new TaskCompletionSource<object>();
    return tcs.Task;
}

Vorausgesetzt, das asyncist nicht obligatorisch, ist dies eine Koroutine oder eine normale Funktion? Sollte der Compiler es als Coroutine umschreiben oder nicht? Beides könnte mit unterschiedlicher Semantik möglich sein.

Laurent LA RIZZA
quelle