2010-10-13 2 views
5

Je comprends qu'il s'agit d'un détail d'implémentation. Je suis en fait curieux de savoir ce que le détail de l'implémentation est dans Microsoft CLR. Maintenant, portez-moi car je n'ai pas étudié CS à l'université, donc j'ai peut-être manqué quelques principes fondamentaux. Mais ma compréhension de la "pile" et du "tas" telle qu'elle est implémentée dans le CLR tel qu'il est aujourd'hui est, je pense, solide. Je ne vais pas faire une déclaration parapluie inexacte comme "les types de valeur sont stockés sur la pile," par exemple. Mais, dans la plupart des scénarios courants - variables locales de vanille, de type valeur, passées en paramètres ou déclarées dans la méthode et non contenues dans une fermeture - les variables de type valeur sont stockées sur la pile (encore une fois, dans CLR de Microsoft).Où sont stockés les paramètres de type valeur de référence pour les appels de méthode asynchrones dans CLR de Microsoft?

Je suppose que ce que je ne suis pas sûr de est où ref valeur des paramètres de type sont disponibles dans

origine ce que je pensais était que, si la pile d'appel ressemble à ceci (à gauche = bas).

A() -> B() -> C() 

... alors une variable locale déclarée dans le cadre de a et passé en paramètre ref à B pourrait encore être stockés sur la pile - il ne pouvions pas? B aurait simplement besoin de l'emplacement de la mémoire où cette variable locale a été stockée A cadre (pardonnez-moi si ce n'est pas la bonne terminologie, je pense que ce que je veux dire, de toute façon).

Je réalise cela ne pouvait être strictement vrai, cependant, quand il me est apparu que je pouvais faire:

delegate void RefAction<T>(ref T arg); 

void A() 
{ 
    int x = 100; 

    RefAction<int> b = B; 

    // This is a non-blocking call; A will return immediately 
    // after this. 
    b.BeginInvoke(ref x, C, null); 
} 

void B(ref int arg) 
{ 
    // Putting a sleep here to ensure that A has exited by the time 
    // the next line gets executed. 
    Thread.Sleep(1000); 

    // Where is arg stored right now? The "x" variable 
    // from the "A" method should be out of scope... but its value 
    // must somehow be known here for this code to make any sense. 
    arg += 1; 
} 

void C(IAsyncResult result) 
{ 
    var asyncResult = (AsyncResult)result; 
    var action = (RefAction<int>)asyncResult.AsyncDelegate; 

    int output = 0; 

    // This variable originally came from A... but then 
    // A returned, it got updated by B, and now it's still here. 
    action.EndInvoke(ref output, result); 

    // ...and this prints "101" as expected (?). 
    Console.WriteLine(output); 
} 

Ainsi, dans l'exemple ci-dessus, où est x (dans A « s portée) stocké? Et comment ça marche? Est-ce que c'est en boîte? Si non, est-il sujet à la collecte des ordures maintenant, en dépit d'être un type de valeur? Ou la mémoire peut-elle être immédiatement récupérée?

Je m'excuse pour la question longue. Mais même si la réponse est assez simple, peut-être que cela sera instructif pour les autres qui se retrouvent à se demander la même chose à l'avenir.

+0

http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx – Brian

Répondre

4

Je ne crois pas que lorsque vous utilisez BeginInvoke() et EndInvoke() avec ref ou out arguments que vous êtes vraiment passer variables par ref. Le fait que nous devons appeler EndInvoke() avec un paramètre ref devrait aussi être un indice à cela.

Changeons votre exemple pour démontrer le comportement que je décris:

void A() 
{ 
    int x = 100; 
    int z = 400; 

    RefAction<int> b = B; 

    //b.BeginInvoke(ref x, C, null); 
    var ar = b.BeginInvoke(ref x, null, null); 
    b.EndInvoke(ref z, ar); 

    Console.WriteLine(x); // outputs '100' 
    Console.WriteLine(z); // outputs '101' 
} 

Si vous examinez la sortie maintenant, vous verrez que la valeur de x est en réalité inchangée. Mais z contient maintenant la valeur de mise à jour.

Je soupçonne que le compilateur modifie la sémantique du passage des variables par ref lorsque vous utilisez les méthodes Begin/EndInvoke asynchrones.

Après avoir pris un coup d'oeil à l'IL produit par ce code, il semble que ref arguments à BeginInvoke() sont encore passés by ref. Bien que Reflector ne montre pas l'IL pour cette méthode, je suppose qu'il ne transmet tout simplement pas le paramètre sous la forme d'un argument ref, mais crée à la place une variable distincte dans les coulisses pour passer à B(). Lorsque vous appelez ensuite EndInvoke(), vous devez à nouveau fournir un argument ref pour extraire la valeur de l'état asynchrone. Il est probable que de tels arguments sont réellement stockés dans le cadre de (ou conjointement avec) l'objet IAsyncResult qui est nécessaire pour récupérer leurs valeurs. Réfléchissons à la raison pour laquelle le comportement fonctionne probablement de cette façon. Lorsque vous effectuez un appel asynchrone à une méthode, vous le faites sur un thread distinct.Ce thread a sa propre pile et ne peut donc pas utiliser le mécanisme typique d'aliasing ref/out variables. Cependant, pour obtenir des valeurs renvoyées à partir d'une méthode asynchrone, vous devez éventuellement appeler EndInvoke() pour terminer l'opération et récupérer ces valeurs. Cependant, l'appel à EndInvoke() pourrait tout aussi bien se produire sur un fil complètement différent que l'appel original à BeginInvoke() ou le corps réel de la méthode. Il est clair que la pile des appels n'est pas un bon endroit pour stocker de telles données - d'autant plus que le thread utilisé pour l'appel asynchrone pourrait être réutilisé pour une méthode différente une fois l'opération asynchrone terminée. Par conséquent, un mécanisme autre que la pile est nécessaire pour "marshaler" la valeur de retour et les arguments out/ref de la méthode rappelée sur le site où ils seront finalement utilisés.

Je crois que ce mécanisme (dans l'implémentation Microsoft .NET) est l'objet IAsyncResult. En fait, si vous examinez l'objet IAsyncResult dans le débogueur, vous remarquerez que dans les membres non publics il existe _replyMsg, qui contient une collection Properties. Cette collection contient des éléments tels que __OutArgs et __Return dont les données semblent refléter leur nom.

EDIT:est ici une théorie sur la conception des délégués async, qui se produit pour moi. Il semble probable que les signatures BeginInvoke() et EndInvoke() ont été choisies pour être aussi semblables que possible les unes aux autres pour éviter la confusion et améliorer la clarté. La méthode n'a pas réellement besoin pour accepter ref/out arguments - car il a seulement besoin de leur valeur ... pas leur identité (car il ne va jamais leur assigner quoi que ce soit). Cependant il serait vraiment étrange (par exemple) d'avoir un appel BeginInvoke() qui prend un int et un appel EndInvoke() qui prend un ref int. Maintenant, il est possible qu'il y ait des raisons techniques pour lesquelles les appels début/fin devraient avoir des signatures identiques - mais je pense que les avantages de la clarté et de la symétrie sont suffisants pour valider une telle conception. Tout ceci est, bien sûr, un détail d'implémentation du compilateur CLR et C# et pourrait changer à l'avenir. Il est intéressant, cependant, qu'il existe une possibilité de confusion - si vous pensez que la variable d'origine passée à BeginInvoke() sera réellement modifiée. Il souligne également l'importance d'appeler EndInvoke() pour terminer une opération asynchrone. Peut-être que quelqu'un de l'équipe C# (s'ils voient cette question) pourrait offrir plus de perspicacité dans les détails et les choix de conception derrière cette fonctionnalité.

+0

Wow, super test. Je n'ai même pas pensé à essayer cela (en fait, je supposais * que * si j'appelais «EndInvoke» dans le cadre de ** A **, cela invaliderait mes conclusions car toute la question dont j'étais incertain était comment le paramètre 'ref' est stocké une fois que l'image de ** A ** n'est plus disponible)! C'est marrant, cependant; ceci semble éclaircir un point de confusion (le paramètre 'ref' ne pointe clairement pas sur l'emplacement de la variable d'origine) en échange d'un autre (donc un paramètre' ref' transmis à 'BeginInvoke' n'est pas vraiment' ref 'paramètre du tout?). –

+0

@Dan Tao: Comme je l'ai mentionné plus haut, l'IL du réflecteur indique que les arguments 'ref' de' BeginInvoke() 'sont effectivement passés par ref. Je soupçonne toutefois que 'BeginInvoke()' en interne fait une copie de la valeur dans l'objet 'IAsyncResult' et passe la copie' par ref' à 'B()'. En fin de compte, seul 'A()' peut observer l'incohérence ici, s'il choisit de passer une variable autre que 'x' quand on appelle' EndInvoke() '. – LBushkin

+0

Oui, comme Hans le mentionne dans sa réponse mise à jour (si je le comprends bien), l'appel de 'BeginInvoke' reçoit un paramètre' ref' qui pointe vers l'emplacement d'une * copie * de la variable d'origine. J'ai aussi testé ceci avec une variable non-locale, en fait - un champ d'instance - et vu le même comportement (donc ce n'est pas seulement le comportement qui compte dans cet exemple): passer le champ comme un 'ref' paramètre à un appel 'BeginInvoke' n'a pas réellement changé la valeur du champ. –

2

Regardez le code généré avec le réflecteur pour le savoir. Je suppose qu'une classe anonyme contenant x est générée, comme lorsque vous utilisez des fermetures (expressions lambda qui référencent des variables dans le cadre de la pile en cours). Oubliez cela et lisez les autres réponses.

+0

Cela ne semble pas être le cas. Voir ma réponse pour plus de détails. – LBushkin

+0

@LBushkin: Aaaaaaaaaah. Donc, ma supposition est fausse. –

3

Le CLR est complètement hors de la boucle sur ceci, c'est le travail du compilateur JIT pour générer le code machine approprié pour obtenir un argument passé par référence. Qui est un détail de mise en œuvre en soi, il y a différents jitters pour différentes architectures de machines.

Mais les communs le font exactement comme un programmeur en C le fait, ils passent un pointeur vers la variable. Ce pointeur est transmis dans un registre de CPU ou dans le cadre de la pile, en fonction du nombre d'arguments pris par la méthode.Lorsque la variable lives n'a pas d'importance, un pointeur vers une variable dans le cadre de la pile de l'appelant est tout aussi valide qu'un pointeur vers un membre d'un objet de type référentiel stocké sur le tas. Le garbage collector connaît la différence entre eux, grâce à la valeur du pointeur, ajustant le pointeur si nécessaire quand il déplace un objet.

Votre fragment de code invoque la magie dans le framework .NET qui est nécessaire pour effectuer des appels de marshaling d'un thread à un autre. C'est le même type de plomberie qui fait fonctionner Remoting. Pour effectuer un tel appel, un nouveau cadre de pile doit être créé sur le thread où l'appel est effectué. Le code distant utilise la définition de type du délégué pour savoir à quoi doit ressembler ce cadre de pile. Et il peut traiter des arguments passés par référence, il sait qu'il doit allouer un emplacement dans le cadre de la pile pour stocker la variable pointée, i dans votre cas. L'appel BeginInvoke initialise la copie de la variable i dans le cadre de la pile distante. La même chose arrive lors de l'appel EndInvoke(), les résultats sont copiés à partir du cadre de la pile dans le thread threadpool. Le point clé est qu'il n'y a pas réellement un pointeur vers la variable i, il y a un pointeur vers la copie de celui-ci.

Pas si sûr que cette réponse est très claire, ayant une certaine compréhension de la façon dont les processeurs fonctionnent et un peu de connaissances C de sorte que le concept d'un pointeur est en cristal peut aider beaucoup.

+1

Je pense que l'exemple OPs est construit de sorte que le cadre de pile de 'A()' ne soit plus disponible. D'où la question de savoir comment la variable est passée par ref à la méthode asynchrone. – LBushkin

+0

Merci @LBushkin, j'ai raté ça. Post mis à jour. –

+0

Le JITter n'est-il pas considéré comme faisant partie du Common Language Runtime (qui est lui-même une implémentation du CLI Virtual Execution System, voir Ecma-335 §12)? – Frank

Questions connexes