Domanda

Riepilogo: C # /. NET dovrebbe essere raccolto. C # ha un distruttore, usato per pulire le risorse. Cosa succede quando un oggetto A viene raccolto nella stessa spazzatura e provo a clonare uno dei suoi membri variabili? Apparentemente, sui multiprocessori, a volte, il Garbage Collector vince ...

Il problema

Oggi, durante una sessione di formazione su C #, l'insegnante ci ha mostrato del codice che conteneva un bug solo quando eseguito su multiprocessori.

Riassumo per dire che a volte, il compilatore o il JIT si rovinano chiamando il finalizzatore di un oggetto di classe C # prima di tornare dal suo metodo chiamato.

Il codice completo, fornito nella documentazione di Visual C ++ 2005, verrà pubblicato come " answer " per evitare di porre domande molto grandi, ma gli elementi essenziali sono di seguito:

La seguente classe ha un " Hash " proprietà che restituirà una copia clonata di un array interno. At is construction, il primo elemento dell'array ha un valore di 2. Nel distruttore, il suo valore è impostato su zero.

Il punto è: se provi a ottenere il " Hash " proprietà di " Esempio " ;, otterrai una copia pulita dell'array, il cui primo elemento è ancora 2, poiché l'oggetto viene utilizzato (e come tale, non essendo garbage collection / finalizzato):

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);
        }
    }
}

Ma niente è così semplice ... Il codice che utilizza questa classe si trova all'interno di un thread e, naturalmente, per il test, l'app è fortemente 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();
}

Il metodo statico DoWork è il codice in cui si verifica il problema:

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
    }
}

Una volta ogni 1.000.000 di escrezioni di DoWork, a quanto pare, il Garbage Collector fa la sua magia e cerca di recuperare "ex", poiché non è più indicato nel codice rimanente della funzione e, questa volta, è più veloce di il "Hash" ottieni il metodo. Quindi quello che abbiamo alla fine è un clone di un array di byte a zero, invece di avere quello giusto (con il primo elemento a 2).

La mia ipotesi è che ci sia un allineamento del codice, che essenzialmente sostituisce la linea contrassegnata [1] nella funzione DoWork con qualcosa del tipo:

    // 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();

Se supponiamo che Hash2 sia un semplice accedente codificato come:

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

Quindi, la domanda è: Questo dovrebbe funzionare in C # /. NET o potrebbe essere considerato un bug del compilatore di JIT?

modifica

Vedi i blog di Chris Brumme e Chris Lyons per una spiegazione.

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

La risposta di tutti è stata interessante, ma non ho potuto scegliere una migliore dell'altra. Quindi ti ho dato un +1 ...

Siamo spiacenti

: -)

Modifica 2

Non sono stato in grado di riprodurre il problema su Linux / Ubuntu / Mono, nonostante l'utilizzo dello stesso codice nelle stesse condizioni (più stessi eseguibili in esecuzione contemporaneamente, modalità di rilascio, ecc.)

È stato utile?

Soluzione

È semplicemente un bug nel tuo codice: i finalizzatori non dovrebbero accedere agli oggetti gestiti.

L'unico motivo per implementare un finalizzatore è liberare risorse non gestite. E in questo caso, è necessario implementare attentamente il modello IDisposable standard .

Con questo modello, si implementa un metodo protetto "Dispose protetto (smaltimento bool)". Quando questo metodo viene chiamato dal finalizzatore, pulisce le risorse non gestite, ma non tenta di ripulire le risorse gestite.

Nel tuo esempio, non hai risorse non gestite, quindi non dovresti implementare un finalizzatore.

Altri suggerimenti

Quello che vedi è perfettamente naturale.

Non si mantiene un riferimento all'oggetto che possiede l'array di byte, quindi quell'oggetto (non l'array di byte) è effettivamente gratuito per il garbage collector da raccogliere.

Il Garbage Collector può davvero essere così aggressivo.

Quindi, se chiami un metodo sul tuo oggetto, che restituisce un riferimento a una struttura di dati interna e il finalizzatore per il tuo oggetto confonde quella struttura di dati, devi mantenere anche un riferimento attivo all'oggetto.

Il Garbage Collector vede che la variabile ex non è più utilizzata in quel metodo, quindi può, e come notate, la raccoglierà nei casi giusti (es. tempistica e necessità).

Il modo corretto per farlo è chiamare GC.KeepAlive su ex, quindi aggiungi questa riga di codice in fondo al tuo metodo e tutto dovrebbe andare bene:

GC.KeepAlive(ex);

Ho imparato a conoscere questo comportamento aggressivo leggendo il libro Programmazione di .NET Framework applicata di Jeffrey Richter.

sembra una condizione di competizione tra il tuo thread di lavoro e i thread GC; per evitarlo, penso che ci siano due opzioni:

(1) cambia la tua istruzione if per usare ex.Hash [0] invece di res, in modo che ex non possa essere GC prematuramente, oppure

(2) blocca ex per la durata della chiamata a Hash

questo è un esempio piuttosto spiccio: il punto dell'insegnante era che potrebbe esserci un bug nel compilatore JIT che si manifesta solo su sistemi multicore o che questo tipo di codifica può avere condizioni di gara sottili con Garbage Collection?

Penso che ciò che stai vedendo sia un comportamento ragionevole dovuto al fatto che le cose sono in esecuzione su più thread. Questo è il motivo del metodo GC.KeepAlive (), che dovrebbe essere usato in questo caso per dire al GC che l'oggetto è ancora in uso e che non è un candidato per la pulizia.

Esaminando la funzione DoWork nel tuo "codice completo" risposta, il problema è che subito dopo questa riga di codice:

byte[] res = ex.Hash;

la funzione non fa più alcun riferimento all'oggetto ex , quindi diventa idoneo per la garbage collection a quel punto. L'aggiunta della chiamata a GC.KeepAlive impedirebbe che ciò accada.

Sì, questo è un issue che ha vieni prima .

È ancora più divertente in quanto è necessario eseguire il rilascio affinché ciò accada e si finisce per stringere la testa andando "eh, come può essere nullo?".

Interessante commento dal blog di 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 {
         ...
      }
   }
}

Quindi non possiamo dire per quanto tempo & # 8216; aC & # 8217; potrebbe vivere nel codice sopra. JIT potrebbe riportare il riferimento fino al completamento di Other.work (). Potrebbe incorporare Other.work () in qualche altro metodo e riportare aC ancora più a lungo. Anche se aggiungi & # 8220; aC = null; & # 8221; dopo il suo utilizzo, la JIT è libera di considerare questo compito come codice morto ed eliminarlo. Indipendentemente da quando la JIT interrompe la segnalazione del riferimento, il GC potrebbe non essere in grado di raccoglierlo per un po 'di tempo.

È più interessante preoccuparsi del primo punto in cui un AC potrebbe essere raccolto. Se sei come la maggior parte delle persone, indovinerai che il più presto un AC diventa idoneo per la raccolta è alla parentesi graffa di chiusura di Other.work () & # 8217; s & # 8220; if & # 8221; clausola, in cui ho aggiunto il commento. In effetti, nell'IL non esistono parentesi graffe. Sono un contratto sintattico tra te e il tuo compilatore di lingue. Other.work () è libero di interrompere la segnalazione di aC non appena ha avviato la chiamata a aC.m ().

È assolutamente normale che il finalizzatore venga chiamato nel metodo do work come dopo il es. Hash call, il CLR sa che l'istanza ex non sarà più necessaria ...

Ora, se vuoi mantenere viva l'istanza, fai questo:

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 non fa ... niente :) è un metodo vuoto non inlinabile / jittable il cui unico scopo è indurre il GC a pensare che l'oggetto verrà usato dopo questo.

ATTENZIONE: il tuo esempio è perfettamente valido se il metodo DoWork fosse un metodo C ++ gestito ... DO devi mantenere manualmente attive le istanze gestite se non vuoi che il distruttore sia chiamato da un altro thread. IE. si passa un riferimento a un oggetto gestito che eliminerà un BLOB di memoria non gestita quando finalizzato e il metodo utilizza questo stesso BLOB. Se non mantieni viva l'istanza, avrai una condizione di competizione tra il GC e il thread del tuo metodo.

E questo finirà in lacrime. E gestito il danneggiamento dell'heap ...

Il codice completo

Di seguito troverai il codice completo, copiato / incollato da un file .cs di Visual C ++ 2008. Dato che ora sono su Linux e senza alcun compilatore Mono o conoscenza del suo utilizzo, non c'è modo di fare test ora. Tuttavia, un paio d'ore fa, ho visto questo codice funzionare e il suo 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();
    }
}

Per chi fosse interessato, posso inviare il progetto compresso tramite e-mail.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top