Je répondrai à vos questions spécifiques ci-dessous, mais vous feriez probablement bien de simplement lire mes articles détaillés sur la façon dont nous avons conçu le rendement et l'attente.
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/
Certains de ces articles sont désormais obsolètes; le code généré est différent à bien des égards. Mais ceux-ci vous donneront certainement une idée de son fonctionnement.
De plus, si vous ne comprenez pas comment les lambdas sont générées en tant que classes de fermeture, comprenez-le d' abord . Vous ne ferez pas des têtes ou des queues d'async si vous n'avez pas de lambdas vers le bas.
Lorsqu'une attente est atteinte, comment le moteur d'exécution sait-il quel morceau de code doit être exécuté ensuite?
await
est généré comme:
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
C'est fondamentalement ça. Attendre est juste un retour de fantaisie.
Comment sait-il quand il peut reprendre là où il s'est arrêté et comment se souvient-il où?
Eh bien, comment faites-vous cela sans attendre? Lorsque la méthode foo appelle method bar, nous nous rappelons comment revenir au milieu de foo, avec tous les locaux de l'activation de foo intacts, peu importe ce que fait la barre.
Vous savez comment cela se fait en assembleur. Un enregistrement d'activation pour foo est poussé sur la pile; il contient les valeurs des habitants. Au moment de l'appel, l'adresse de retour dans foo est poussée sur la pile. Lorsque la barre est terminée, le pointeur de pile et le pointeur d'instruction sont réinitialisés là où ils doivent être et foo continue à partir de là où il s'était arrêté.
La suite d'une attente est exactement la même, sauf que l'enregistrement est placé sur le tas pour la raison évidente que la séquence d'activations ne forme pas une pile .
Le délégué qui attend donne comme suite à la tâche contient (1) un nombre qui est l'entrée d'une table de recherche qui donne le pointeur d'instruction que vous devez exécuter ensuite, et (2) toutes les valeurs des locaux et des temporaires.
Il y a du matériel supplémentaire là-dedans; par exemple, dans .NET, il est illégal de créer une branche au milieu d'un bloc try, vous ne pouvez donc pas simplement coller l'adresse du code à l'intérieur d'un bloc try dans la table. Mais ce sont des détails de comptabilité. Conceptuellement, l'enregistrement d'activation est simplement déplacé sur le tas.
Qu'arrive-t-il à la pile d'appels actuelle, est-elle sauvegardée d'une manière ou d'une autre?
Les informations pertinentes dans l'enregistrement d'activation actuel ne sont jamais placées sur la pile en premier lieu; il est alloué du tas dès le départ. (Eh bien, les paramètres formels sont normalement passés sur la pile ou dans des registres, puis copiés dans un emplacement de tas lorsque la méthode commence.)
Les enregistrements d'activation des appelants ne sont pas stockés; l'attente va probablement leur revenir, rappelez-vous, donc ils seront traités normalement.
Notez qu'il s'agit d'une différence significative entre le style de passage de continuation simplifié de await et les véritables structures d'appel avec continuation en cours que vous voyez dans des langages comme Scheme. Dans ces langues, toute la suite, y compris la continuation vers les appelants, est capturée par call-cc .
Que faire si la méthode appelante effectue d'autres appels de méthode avant d'attendre, pourquoi la pile n'est-elle pas écrasée?
Ces appels de méthode retournent, et donc leurs enregistrements d'activation ne sont plus sur la pile au moment de l'attente.
Et comment diable le runtime fonctionnerait-il à travers tout cela dans le cas d'une exception et d'une pile se dérouler?
Dans le cas d'une exception non interceptée, l'exception est interceptée, stockée dans la tâche et relancée lorsque le résultat de la tâche est récupéré.
Vous vous souvenez de toute cette comptabilité que j'ai mentionnée auparavant? Obtenir une sémantique d'exception correcte était une énorme douleur, laissez-moi vous dire.
Lorsque le rendement est atteint, comment le runtime garde-t-il une trace du point où les choses doivent être ramassées? Comment l'état de l'itérateur est-il préservé?
De la même façon. L'état des locaux est déplacé sur le tas, et un nombre représentant l'instruction à laquelle MoveNext
doit reprendre la prochaine fois qu'elle est appelée est stocké avec les locaux.
Et encore une fois, il y a un tas d'équipement dans un bloc d'itérateur pour s'assurer que les exceptions sont gérées correctement.