Ich werde Ihre spezifischen Fragen unten beantworten, aber Sie sollten wahrscheinlich einfach meine ausführlichen Artikel darüber lesen, wie wir Ertrag entworfen haben und auf ihn warten.
https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/
https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/
https://blogs.msdn.microsoft.com/ericlippert/tag/async/
Einige dieser Artikel sind jetzt veraltet. Der generierte Code unterscheidet sich in vielerlei Hinsicht. Aber diese geben Ihnen sicherlich eine Vorstellung davon, wie es funktioniert.
Wenn Sie nicht verstehen, wie Lambdas als Abschlussklassen generiert werden, verstehen Sie dies zuerst . Sie werden keine asynchronen Köpfe oder Schwänze machen, wenn Sie keine Lambdas haben.
Woher weiß die Laufzeit, wenn ein Warten erreicht ist, welcher Code als nächstes ausgeführt werden soll?
await
wird generiert als:
if (the task is not completed)
assign a delegate which executes the remainder of the method as the continuation of the task
return to the caller
else
execute the remainder of the method now
Das ist es im Grunde. Warten ist nur eine schicke Rückkehr.
Woher weiß es, wann es dort weitermachen kann, wo es aufgehört hat, und woher erinnert es sich, wo es aufgehört hat?
Nun, wie machst du das ohne zu warten? Wenn method foo method bar aufruft, erinnern wir uns irgendwie daran, wie wir zur Mitte von foo zurückkehren können, wobei alle Einheimischen der Aktivierung von foo intakt sind, egal welche Bar dies tut.
Sie wissen, wie das im Assembler gemacht wird. Ein Aktivierungsdatensatz für foo wird auf den Stapel verschoben. es enthält die Werte der Einheimischen. Zum Zeitpunkt des Aufrufs wird die Rücksprungadresse in foo auf den Stapel geschoben. Wenn der Balken fertig ist, werden der Stapelzeiger und der Anweisungszeiger auf die Position zurückgesetzt, an der sie sich befinden müssen, und foo fährt dort fort, wo er aufgehört hat.
Die Fortsetzung eines Wartens ist genau das gleiche, außer dass der Datensatz auf den Heap gelegt wird, aus dem offensichtlichen Grund, dass die Aktivierungssequenz keinen Stapel bildet .
Der wartende Delegat, der als Fortsetzung der Aufgabe angibt, enthält (1) eine Zahl, die die Eingabe in eine Nachschlagetabelle ist, die den Befehlszeiger enthält, den Sie als Nächstes ausführen müssen, und (2) alle Werte von Einheimischen und Provisorien.
Es gibt dort einige zusätzliche Ausrüstung; In .NET ist es beispielsweise illegal, in die Mitte eines try-Blocks zu verzweigen, sodass Sie die Adresse des Codes nicht einfach in einen try-Block in die Tabelle einfügen können. Dies sind jedoch Details zur Buchhaltung. Konzeptionell wird der Aktivierungsdatensatz einfach auf den Heap verschoben.
Was passiert mit dem aktuellen Aufrufstapel, wird er irgendwie gespeichert?
Die relevanten Informationen im aktuellen Aktivierungsdatensatz werden niemals in erster Linie auf den Stapel gelegt. Es wird von Anfang an vom Heap zugewiesen. (Nun, formale Parameter werden normalerweise auf dem Stapel oder in Registern übergeben und dann zu Beginn der Methode in einen Heap-Speicherort kopiert.)
Die Aktivierungsaufzeichnungen der Anrufer werden nicht gespeichert. Das Warten wird wahrscheinlich zu ihnen zurückkehren, denken Sie daran, damit sie normal behandelt werden.
Beachten Sie, dass dies ein deutlicher Unterschied zwischen dem vereinfachten Wartestil für das Weiterleiten von Fortsetzungen und echten Call-with-Current-Continuation-Strukturen ist, die Sie in Sprachen wie Scheme sehen. In diesen Sprachen wird die gesamte Fortsetzung einschließlich der Fortsetzung zurück in die Anrufer von call-cc erfasst .
Was ist, wenn die aufrufende Methode andere Methodenaufrufe ausführt, bevor sie wartet? Warum wird der Stapel nicht überschrieben?
Diese Methodenaufrufe kehren zurück, sodass sich ihre Aktivierungsdatensätze zum Zeitpunkt des Wartens nicht mehr auf dem Stapel befinden.
Und wie um alles in der Welt würde sich die Laufzeit im Falle einer Ausnahme und eines Abwickelns eines Stapels durch all dies arbeiten?
Im Falle einer nicht erfassten Ausnahme wird die Ausnahme abgefangen, in der Aufgabe gespeichert und erneut ausgelöst, wenn das Ergebnis der Aufgabe abgerufen wird.
Erinnerst du dich an all die Buchhaltung, die ich zuvor erwähnt habe? Es war ein großer Schmerz, die Ausnahmesemantik richtig zu machen.
Wie verfolgt die Laufzeit den Punkt, an dem die Dinge abgeholt werden sollen, wenn der Ertrag erreicht ist? Wie bleibt der Iteratorstatus erhalten?
Gleicher Weg. Der Zustand der Einheimischen wird auf den Haufen verschoben, und eine Zahl, die die Anweisung darstellt, bei derMoveNext
der nächste Aufruf fortgesetzt werden soll, wird zusammen mit den Einheimischen gespeichert.
Und wieder gibt es eine Menge Ausrüstung in einem Iteratorblock, um sicherzustellen, dass Ausnahmen korrekt behandelt werden.