Gestern hielt ich einen Vortrag über die neue C # "Async" -Funktion, insbesondere über das Aussehen des generierten Codes und the GetAwaiter()
/ BeginAwait()
/ EndAwait()
Aufrufe.
Wir haben uns die vom C # -Compiler generierte Zustandsmaschine genauer angesehen, und es gab zwei Aspekte, die wir nicht verstehen konnten:
- Warum die generierte Klasse eine
Dispose()
Methode und eine$__disposing
Variable enthält, die scheinbar nie verwendet werden (und die Klasse nicht implementiertIDisposable
). - Warum die interne
state
Variable vor jedem Aufruf von aufEndAwait()
0 gesetzt wird, wenn 0 normalerweise "dies ist der erste Einstiegspunkt" bedeutet.
Ich vermute, der erste Punkt könnte durch etwas Interessanteres innerhalb der asynchronen Methode beantwortet werden, obwohl ich mich freuen würde, wenn jemand weitere Informationen hätte. Bei dieser Frage geht es jedoch eher um den zweiten Punkt.
Hier ist ein sehr einfacher Beispielcode:
using System.Threading.Tasks;
class Test
{
static async Task<int> Sum(Task<int> t1, Task<int> t2)
{
return await t1 + await t2;
}
}
... und hier ist der Code, der für die MoveNext()
Methode generiert wird, die die Zustandsmaschine implementiert. Dies wird direkt von Reflector kopiert - ich habe die unaussprechlichen Variablennamen nicht korrigiert:
public void MoveNext()
{
try
{
this.$__doFinallyBodies = true;
switch (this.<>1__state)
{
case 1:
break;
case 2:
goto Label_00DA;
case -1:
return;
default:
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
break;
}
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
this.<>1__state = 2;
this.$__doFinallyBodies = false;
if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
Label_00DA:
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
this.<>1__state = -1;
this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
}
catch (Exception exception)
{
this.<>1__state = -1;
this.$builder.SetException(exception);
}
}
Es ist lang, aber die wichtigen Zeilen für diese Frage sind folgende:
// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
In beiden Fällen wird der Status danach erneut geändert, bevor er das nächste Mal offensichtlich beobachtet wird. Warum also überhaupt auf 0 setzen? Wenn MoveNext()
an dieser Stelle erneut aufgerufen würde (entweder direkt oder über Dispose
), würde die asynchrone Methode effektiv erneut gestartet, was meines Erachtens völlig unangemessen wäre. Wenn und MoveNext()
wird nicht aufgerufen, ist die Zustandsänderung irrelevant.
Ist dies einfach ein Nebeneffekt des Compilers, der den Code zur Generierung von Iteratorblöcken für Async wiederverwendet, wo er möglicherweise eine offensichtlichere Erklärung hat?
Wichtiger Haftungsausschluss
Offensichtlich ist dies nur ein CTP-Compiler. Ich gehe davon aus, dass sich die Dinge vor der endgültigen Veröffentlichung ändern werden - und möglicherweise sogar vor der nächsten CTP-Veröffentlichung. Diese Frage versucht in keiner Weise zu behaupten, dass dies ein Fehler im C # -Compiler oder ähnliches ist. Ich versuche nur herauszufinden, ob es einen subtilen Grund dafür gibt, den ich verpasst habe :)
quelle
Antworten:
Okay, ich habe endlich eine echte Antwort. Ich habe es irgendwie selbst herausgefunden, aber erst nachdem Lucian Wischik vom VB-Teil des Teams bestätigt hatte, dass es wirklich einen guten Grund dafür gibt. Vielen Dank an ihn - und bitte besuchen Sie seinen Blog , der rockt.
Der Wert 0 ist hier nur etwas Besonderes, da es sich nicht um einen gültigen Zustand handelt, in dem Sie sich im
await
Normalfall möglicherweise kurz vor dem befinden . Insbesondere ist es kein Zustand, auf den die Zustandsmaschine möglicherweise an anderer Stelle testet. Ich glaube, dass die Verwendung eines nicht positiven Werts genauso gut funktionieren würde: -1 wird dafür nicht verwendet, da es logisch falsch ist, da -1 normalerweise "fertig" bedeutet. Ich könnte argumentieren, dass wir im Moment 0 eine zusätzliche Bedeutung geben, aber letztendlich spielt es keine Rolle. Bei dieser Frage ging es darum herauszufinden, warum der Staat überhaupt eingestellt wird.Der Wert ist relevant, wenn das Warten in einer Ausnahme endet, die abgefangen wird. Wir können am Ende wieder auf dieselbe Warteerklärung zurückkommen, aber wir dürfen uns nicht in dem Zustand befinden, der bedeutet "Ich bin gerade dabei, von dieser Wartezeit zurückzukommen", da sonst alle Arten von Code übersprungen würden. Es ist am einfachsten, dies anhand eines Beispiels zu zeigen. Beachten Sie, dass ich jetzt das zweite CTP verwende, sodass sich der generierte Code geringfügig von dem in der Frage unterscheidet.
Hier ist die asynchrone Methode:
Konzeptionell
SimpleAwaitable
kann das alles erwarten - vielleicht eine Aufgabe, vielleicht etwas anderes. Für die Zwecke meiner Tests wird immer false für zurückgegebenIsCompleted
und eine Ausnahme ausgelöstGetResult
.Hier ist der generierte Code für
MoveNext
:Ich musste umziehen
Label_ContinuationPoint
, um einen gültigen Code zu erhalten - ansonsten ist er nicht im Umfang dergoto
Anweisung enthalten -, aber das hat keinen Einfluss auf die Antwort.Überlegen Sie, was passiert, wenn
GetResult
die Ausnahme ausgelöst wird. Wir gehen durch den Catch-Block, erhöhen ihni
und drehen ihn dann erneut (vorausgesetzt, eri
ist immer noch kleiner als 3). Wir sind immer noch in dem Zustand, in dem wir uns vor demGetResult
Anruf befanden ... aber wenn wir in dentry
Block kommen, müssen wir "In Try" drucken undGetAwaiter
erneut anrufen ... und das tun wir nur, wenn der Zustand nicht 1 ist. Ohne Bei derstate = 0
Zuweisung wird der vorhandene Kellner verwendet und derConsole.WriteLine
Anruf übersprungen .Es ist ein ziemlich umständliches Stück Code, das durchgearbeitet werden muss, aber das zeigt nur, worüber das Team nachdenken muss. Ich bin froh, dass ich nicht dafür verantwortlich bin :)
quelle
Wenn es auf 1 gehalten würde (erster Fall), würden Sie einen Anruf erhalten,
EndAwait
ohne einen Anruf beiBeginAwait
. Wenn es bei 2 gehalten wird (zweiter Fall), erhalten Sie das gleiche Ergebnis nur für den anderen Kellner.Ich vermute, dass das Aufrufen von BeginAwait false zurückgibt, wenn es bereits gestartet wurde (eine Vermutung von meiner Seite) und den ursprünglichen Wert beibehält, der beim EndAwait zurückgegeben werden soll. Wenn dies der Fall ist, würde es korrekt funktionieren, während Sie, wenn Sie es auf -1 setzen, möglicherweise eine nicht initialisierte
this.<1>t__$await1
für den ersten Fall haben.Dies setzt jedoch voraus, dass BeginAwaiter die Aktion bei keinem Aufruf nach dem ersten tatsächlich startet und in diesen Fällen false zurückgibt. Das Starten wäre natürlich inakzeptabel, da es Nebenwirkungen haben oder einfach zu einem anderen Ergebnis führen könnte. Es wird auch davon ausgegangen, dass der EndAwaiter immer den gleichen Wert zurückgibt, egal wie oft er aufgerufen wird, und dies kann aufgerufen werden, wenn BeginAwait false zurückgibt (gemäß der obigen Annahme).
Es scheint ein Schutz gegen die Rennbedingungen zu sein. Wenn wir die Aussagen einfügen, bei denen movenext von einem anderen Thread nach dem Status = 0 in Fragen aufgerufen wird, sieht es ungefähr so aus wie unten
Wenn die obigen Annahmen korrekt sind, werden einige nicht benötigte Arbeiten ausgeführt, z. B. "Sawiater holen" und denselben Wert <1> t __ $ await1 neu zuweisen. Wenn der Zustand auf 1 gehalten würde, wäre der letzte Teil stattdessen:
Wenn es auf 2 gesetzt wäre, würde die Zustandsmaschine annehmen, dass sie bereits den Wert der ersten Aktion erhalten hat, die nicht wahr ist, und eine (möglicherweise) nicht zugewiesene Variable würde verwendet, um das Ergebnis zu berechnen
quelle
Könnte es etwas mit gestapelten / verschachtelten asynchronen Aufrufen zu tun haben?
dh:
Wird der movenext-Delegat in dieser Situation mehrmals angerufen?
Nur ein Kahn wirklich?
quelle
MoveNext()
würde bei jedem von ihnen einmal angerufen werden.Erklärung der tatsächlichen Zustände:
mögliche Zustände:
Ist es möglich, dass diese Implementierung nur sicherstellen möchte, dass bei einem erneuten Aufruf von MoveNext von jedem Ort aus (während des Wartens) die gesamte Zustandskette von Anfang an erneut bewertet wird, um Ergebnisse neu zu bewerten, die in der Zwischenzeit möglicherweise bereits veraltet sind?
quelle