Question

Résumé: C # /. NET est censé être nettoyé. C # a un destructeur, utilisé pour nettoyer les ressources. Que se passe-t-il lorsqu'un objet A est ramassé dans la même ligne que j'essaie de cloner l'un de ses membres variables? Apparemment, sur les multiprocesseurs, parfois, le ramasse-miettes gagne ...

Le problème

Aujourd'hui, lors d'une session de formation sur C #, le professeur nous a montré un code qui ne contenait un bogue que lorsqu'il était exécuté sur des multiprocesseurs.

Je vais résumer pour dire que parfois, le compilateur ou le JIT échoue en appelant le finaliseur d'un objet de la classe C # avant de revenir de sa méthode appelée.

Le code complet, indiqué dans la documentation de Visual C ++ 2005, sera publié en tant que "réponse". pour éviter de faire une très très grandes questions, mais l'essentiel sont ci-dessous:

La classe suivante a un " hachage " propriété qui retournera une copie clonée d'un tableau interne. Lors de la construction, le premier élément du tableau a la valeur 2. Dans le destructeur, sa valeur est définie sur zéro.

Le problème est le suivant: si vous essayez d’obtenir le " hachage " propriété de "Exemple", vous obtiendrez une copie vierge du tableau, dont le premier élément est toujours 2, lorsque l'objet est utilisé (et en tant que tel, ne pas être nettoyé / finalisé):

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

Mais rien n’est si simple ... Le code utilisant cette classe est inséré dans un thread et, bien sûr, pour le test, l'application est fortement multithread:

public static void Main(string[] args)
{
    Thread t = new Thread(new ThreadStart(ThreadProc));
    t.Start();
    t.Join();
}

private static void ThreadProc()
{
    // running is a boolean which is always true until
    // the user press ENTER
    while (running) DoWork();
}

La méthode statique DoWork est le code à l'origine du problème:

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2)
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
}

Une fois tous les 1 000 000 excutions de DoWork, apparemment, le récupérateur de place fait sa magie et tente de récupérer "ex", car il n'est plus référencé dans le code restant de la fonction, et cette fois, il est plus rapide que le " hachage " obtenir la méthode. Nous avons donc à la fin un clone d’un tableau d’octets édité à zéro, au lieu d’avoir le bon (avec le 1er élément à 2).

Je suppose que le code est en ligne, ce qui remplace essentiellement la ligne marquée [1] dans la fonction DoWork par quelque chose comme:

    // Supposed inlined processing
    byte[] res2 = ex.Hash2;
    // note that after this line, "ex" could be garbage collected,
    // but not res2
    byte[] res = (byte[])res2.Clone();

Si nous supposons que Hash2 est un simple accesseur codé comme suit:

// Hash2 code:
public byte[] Hash2 { get { return (byte[])hashValue; } }

La question est donc: Est-ce que cela est supposé fonctionner de la sorte en C # / .NET, ou est-ce que cela peut être considéré comme un bogue du compilateur du JIT?

modifier

Voir les blogs de Chris Brumme et Chris Lyons pour une explication.

http://blogs.msdn.com/cbrumme /archive/2003/04/19/51365.aspx
http://blogs.msdn.com/clyon/archive/ 2004/09/21 / 232445.aspx

La réponse de chacun était intéressante, mais je ne pouvais pas en choisir une meilleure que l'autre. Alors je vous ai tous donné un +1 ...

Désolé

: -)

Modifier 2

Je n’ai pas pu reproduire le problème sous Linux / Ubuntu / Mono, malgré l’utilisation du même code dans les mêmes conditions (plusieurs exécutables exécutés simultanément, mode de publication, etc.)

Était-ce utile?

La solution

Il s'agit simplement d'un bug dans votre code: les finaliseurs ne doivent pas accéder aux objets gérés.

La mise en œuvre d'un finaliseur n'a pour seul objectif que de libérer des ressources non gérées. Et dans ce cas, vous devez soigneusement appliquer le modèle standard IDisposable .

Avec ce modèle, vous implémentez une méthode protégée, "Dispose de manière protégée". Lorsque cette méthode est appelée à partir du finaliseur, elle nettoie les ressources non gérées, mais ne tente pas de nettoyer les ressources gérées.

Dans votre exemple, vous ne disposez d'aucune ressource non gérée, vous ne devez donc pas implémenter de finaliseur.

Autres conseils

Ce que vous voyez est parfaitement naturel.

Vous ne conservez pas de référence à l'objet qui possède le tableau d'octets. Cet objet (et non le tableau d'octets) est donc libre pour le ramasse-miettes à collecter.

Le ramasse-miettes peut vraiment être aussi agressif.

Ainsi, si vous appelez une méthode sur votre objet, qui renvoie une référence à une structure de données interne, et que le finaliseur de votre objet gâche cette structure de données, vous devez également conserver une référence active à l'objet.

Le ramasse-miettes voit que la variable ex n'est plus utilisée dans cette méthode. Il peut donc, et vous le remarquerez, la ramasser dans les bonnes circonstances (c'est-à-dire le moment et les besoins).

La bonne façon de faire est d’appeler GC.KeepAlive sur ex, ajoutez donc cette ligne de code au bas de votre méthode, et tout devrait bien se passer:

GC.KeepAlive(ex);

J'ai découvert ce comportement agressif en lisant le livre Programmation en cadre .NET appliqué de Jeffrey Richter.

cela ressemble à une situation critique entre votre thread de travail et le (s) thread (s) GC; pour l'éviter, je pense qu'il y a deux options:

(1) modifiez votre instruction if pour qu'elle utilise ex.Hash [0] au lieu de res, de sorte que ex ne puisse pas être GC'd prématurément, ou

(2) verrouillez ex pendant la durée de l'appel au hachage

c’est un exemple assez épineux: l’enseignant a-t-il dit que le compilateur JIT pouvait contenir un bogue qui ne se manifestait que sur des systèmes multicœurs, ou que ce type de codage pouvait avoir des conditions de concurrence subtiles avec le nettoyage des ordures?

Je pense que ce que vous voyez est un comportement raisonnable en raison du fait que les choses fonctionnent sur plusieurs threads. C’est la raison de la méthode GC.KeepAlive (), qui doit être utilisée dans ce cas pour indiquer au GC que l’objet est toujours utilisé et qu’il n’est pas candidat au nettoyage.

En regardant la fonction DoWork dans votre " code complet " réponse, le problème est que, immédiatement après cette ligne de code:

byte[] res = ex.Hash;

la fonction ne fait plus référence à l'objet ex , elle devient donc éligible pour le garbage collection à ce stade. L’ajout de l’appel à GC.KeepAlive empêcherait ce problème.

Oui, il s'agit d'un numéro qui a s'affiche avant .

C'est encore plus amusant en ce sens que vous avez besoin de lancer une release pour que cela se produise et que vous finissiez par vous chamailler la tête "Hein, comment cela peut-il être nul?".

Commentaire intéressant du blog de Chris Brumme

http://blogs.msdn.com/cbrumme /archive/2003/04/19/51365.aspx

class C {<br>
   IntPtr _handle;
   Static void OperateOnHandle(IntPtr h) { ... }
   void m() {
      OperateOnHandle(_handle);
      ...
   }
   ...
}

class Other {
   void work() {
      if (something) {
         C aC = new C();
         aC.m();
         ...  // most guess here
      } else {
         ...
      }
   }
}

Nous ne pouvons donc pas dire combien de temps & # 8216; aC & # 8217; pourrait vivre dans le code ci-dessus. L'ECE peut signaler la référence jusqu'à la fin de Other.work (). Cela pourrait incorporer Other.work () dans une autre méthode, et indiquer aC encore plus longtemps. Même si vous ajoutez & # 8220; aC = null; & # 8221; Après votre utilisation, le JIT est libre de considérer cette assignation comme un code mort et de l'éliminer. Indépendamment du moment où l’ECM arrête de signaler la référence, il se peut que le GC ne la récupère pas avant un certain temps.

Il est plus intéressant de s’inquiéter du moment où l’AC pourrait être collecté le plus tôt possible. Si vous êtes comme la plupart des gens, vous devinerez que l’AC le plus tôt devient éligible pour la collecte se trouve à l’accolade finale d’Autre.work () & # 8217; si & # 8221; clause, où j’ai ajouté le commentaire. En fait, les accolades n’existent pas dans l’IL. Ils constituent un contrat syntaxique entre vous et votre compilateur de langage. Other.work () est libre d'arrêter de signaler aC dès qu'il a lancé l'appel à aC.m ().

C’est parfaitement normal que le finaliseur soit appelé dans votre méthode de travail après le Appel ex.Hash, le CLR sait que l'instance ex ne sera plus nécessaire ...

Maintenant, si vous voulez garder l'instance en vie, procédez comme suit:

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2) // NOTE
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
  GC.KeepAlive(ex); // keep our instance alive in case we need it.. uh.. we don't
}

GC.KeepAlive ne ... rien :) est une méthode vide non incrustable / jittable dont le seul but est de tromper le GC en lui faisant croire que l’objet sera utilisé par la suite.

AVERTISSEMENT: votre exemple est parfaitement valide si la méthode DoWork était une méthode C ++ gérée ... Vous DO devez conserver manuellement les instances gérées en vie si vous ne voulez pas que le destructeur soit appelé de l'intérieur d'un autre thread. C'EST À DIRE. vous transmettez une référence à un objet géré qui va supprimer un blob de mémoire non gérée une fois finalisé, et la méthode utilise ce même blob. Si vous ne tenez pas l'instance en vie, vous allez avoir une situation de concurrence critique entre le GC et le thread de votre méthode.

Et cela finira par pleurer. Et géré la corruption de tas ...

Le code complet

Vous trouverez ci-dessous le code complet, copié / collé à partir d'un fichier .cs de Visual C ++ 2008. Comme je suis maintenant sous Linux, et sans compilateur Mono ni connaissance de son utilisation, je ne peux plus faire de tests maintenant. Pourtant, il y a quelques heures, j'ai vu ce code fonctionner et son bug:

using System;
using System.Threading;

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }
    public byte[] Hash2 { get { return (byte[])hashValue; } }

    public int returnNothing() { return 25; }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

public class Test
{
    private static int totalCount = 0;
    private static int finalizerFirstCount = 0;

    // This variable controls the thread that runs the demo.
    private static bool running = true;

    // In order to demonstrate the finalizer running first, the
    // DoWork method must create an Example object and invoke its
    // Hash property. If there are no other calls to members of
    // the Example object in DoWork, garbage collection reclaims
    // the Example object aggressively. Sometimes this means that
    // the finalizer runs before the call to the Hash property
    // completes. 

    private static void DoWork()
    {
        totalCount++;

        // Create an Example object and save the value of the 
        // Hash property. There are no more calls to members of 
        // the object in the DoWork method, so it is available
        // for aggressive garbage collection.

        Example ex = new Example();

        // Normal processing
        byte[] res = ex.Hash;

        // Supposed inlined processing
        //byte[] res2 = ex.Hash2;
        //byte[] res = (byte[])res2.Clone();

        // successful try to keep reference alive
        //ex.returnNothing();

        // Failed try to keep reference alive
        //ex = null;

        // If the finalizer runs before the call to the Hash 
        // property completes, the hashValue array might be
        // cleared before the property value is read. The 
        // following test detects that.

        if (res[0] != 2)
        {
            finalizerFirstCount++;
            Console.WriteLine("The finalizer ran first at {0} iterations.", totalCount);
        }

        //GC.KeepAlive(ex);
    }

    public static void Main(string[] args)
    {
        Console.WriteLine("Test:");

        // Create a thread to run the test.
        Thread t = new Thread(new ThreadStart(ThreadProc));
        t.Start();

        // The thread runs until Enter is pressed.
        Console.WriteLine("Press Enter to stop the program.");
        Console.ReadLine();

        running = false;

        // Wait for the thread to end.
        t.Join();

        Console.WriteLine("{0} iterations total; the finalizer ran first {1} times.", totalCount, finalizerFirstCount);
    }

    private static void ThreadProc()
    {
        while (running) DoWork();
    }
}

Pour les personnes intéressées, je peux envoyer le projet compressé par courrier électronique.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top