Pregunta

Resumen: C # /. NET se supone que se debe recolectar basura. C # tiene un destructor, usado para limpiar recursos. ¿Qué sucede cuando un objeto A es basura recolectada en la misma línea que trato de clonar uno de sus miembros variables? Aparentemente, en multiprocesadores, a veces, el recolector de basura gana ...

El problema

Hoy, en una sesión de entrenamiento en C #, el profesor nos mostró un código que contenía un error solo cuando se ejecuta en multiprocesadores.

Resumiré para decir que, a veces, el compilador o el JIT se equivocan llamando al finalizador de un objeto de clase C # antes de regresar de su método llamado.

El código completo, que figura en la documentación de Visual C ++ 2005, se publicará como " respuesta " para evitar hacer preguntas muy grandes, pero lo esencial está abajo:

La siguiente clase tiene un " Hash " propiedad que devolverá una copia clonada de una matriz interna. En su construcción, el primer elemento de la matriz tiene un valor de 2. En el destructor, su valor se establece en cero.

El punto es: si intentas obtener el " Hash " propiedad de " Ejemplo " ;, obtendrá una copia limpia de la matriz, cuyo primer elemento aún es 2, ya que el objeto se está utilizando (y como tal, no se recolecta / finaliza):

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

Pero nada es tan simple ... El código que utiliza esta clase está trabajando dentro de un hilo y, por supuesto, para la prueba, la aplicación es multiproceso:

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

El método estático DoWork es el código donde ocurre el 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 vez cada 1,000,000 excuciones de DoWork, aparentemente, el recolector de basura hace su magia, y trata de reclamar " ex " ;, ya que ya no está referenciado en el código restante de la función, y esta vez, es más rápido que el " Hash " obtener metodo Entonces, lo que tenemos al final es un clon de una matriz de bytes de cero ed, en lugar de tener la correcta (con el primer elemento en 2).

Mi conjetura es que hay una incorporación del código, que esencialmente reemplaza la línea marcada [1] en la función DoWork por algo como:

    // 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 supusimos que Hash2 es un simple acceso codificado como:

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

Entonces, la pregunta es: ¿Se supone que esto funciona de esa manera en C # / .NET, o podría considerarse como un error del compilador del JIT?

editar

Consulte los blogs de Chris Brumme y Chris Lyons para obtener una explicación.

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

La respuesta de todos fue interesante, pero no pude elegir una mejor que la otra. Así que les di a todos un +1 ...

Lo siento

:-)

Editar 2

No pude reproducir el problema en Linux / Ubuntu / Mono, a pesar de usar el mismo código en las mismas condiciones (varios ejecutables ejecutándose simultáneamente, modo de lanzamiento, etc.)

¿Fue útil?

Solución

Es simplemente un error en su código: los finalizadores no deberían estar accediendo a objetos gestionados.

La única razón para implementar un finalizador es liberar recursos no administrados. Y en este caso, debe implementar con cuidado el patrón IDisposable estándar .

Con este patrón, implementas un método protegido " protegido Dispose (bool disposing) " ;. Cuando se llama a este método desde el finalizador, limpia los recursos no administrados, pero no intenta limpiar los recursos administrados.

En su ejemplo, no tiene ningún recurso no administrado, por lo que no debe implementar un finalizador.

Otros consejos

Lo que estás viendo es perfectamente natural.

No mantienes una referencia al objeto que posee la matriz de bytes, por lo que ese objeto (no la matriz de bytes) está realmente libre para que el recolector de basura la recoja.

El recolector de basura realmente puede ser tan agresivo.

Entonces, si llama a un método en su objeto, que devuelve una referencia a una estructura de datos interna, y el finalizador de su objeto descompone esa estructura de datos, también necesita mantener una referencia en vivo al objeto.

El recolector de basura ve que la variable ex ya no se usa en ese método, por lo que puede, y como usted notará, la basura la recolectará en las circunstancias adecuadas (es decir, tiempo y necesidad).

La forma correcta de hacer esto es llamar a GC.KeepAlive on ex, así que agregue esta línea de código al final de su método, y todo debería estar bien:

GC.KeepAlive(ex);

Aprendí sobre este comportamiento agresivo al leer el libro Programación de Framework .NET Aplicada por Jeffrey Richter.

esto parece una condición de carrera entre el hilo de trabajo y el (los) hilo (s) del GC; Para evitarlo, creo que hay dos opciones:

(1) cambie su declaración if para usar ex.Hash [0] en lugar de res, de modo que ex no pueda ser GC'd prematuramente, o

(2) bloqueo ex durante la llamada a Hash

ese es un ejemplo bastante elegante: ¿fue el punto del profesor de que puede haber un error en el compilador JIT que solo se manifiesta en sistemas multinúcleo, o que este tipo de codificación puede tener condiciones de carrera sutiles con la recolección de basura?

Creo que lo que estás viendo es un comportamiento razonable debido al hecho de que las cosas se están ejecutando en varios subprocesos. Esta es la razón del método GC.KeepAlive (), que se debe usar en este caso para indicar al GC que el objeto aún se está utilizando y que no es un candidato para la limpieza.

Observando la función DoWork en tu " código completo " Respuesta, el problema es que inmediatamente después de esta línea de código:

byte[] res = ex.Hash;

la función ya no hace ninguna referencia al objeto ex , por lo que se vuelve elegible para la recolección de basura en ese momento. Agregar la llamada a GC.KeepAlive evitaría que esto sucediera.

Sí, este es un problema que tiene aparece antes .

Es aún más divertido en el sentido de que necesitas ejecutar el lanzamiento para que esto suceda y terminas sacudiéndote la cabeza diciendo 'eh, ¿cómo puede ser nulo?'.

Comentario interesante del 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 {
         ...
      }
   }
}

Por lo tanto, no podemos decir cuánto tiempo podría vivir ‘aC’ en el código anterior. El JIT podría informar la referencia hasta que se complete Other.work (). Puede incluir Other.work () en algún otro método e informar aC aún más. Incluso si agrega “aC = null;” después de su uso, el JIT es libre de considerar esta asignación como un código muerto y eliminarlo. Independientemente de cuándo el JIT deje de informar la referencia, es posible que el GC no pueda recopilarla durante algún tiempo.

Es más interesante preocuparse por el primer punto en el que se pueda recopilar aC. Si usted es como la mayoría de las personas, supondrá que lo más pronto posible aC será elegible para la recolección en el corchete de cierre de la cláusula "if" de Other.work (), donde he agregado el comentario. De hecho, las llaves no existen en la IL. Son un contrato sintáctico entre usted y su compilador de idiomas. Other.work () puede dejar de informar aC tan pronto como haya iniciado la llamada a aC.m ().

Eso es perfectamente nornal para que se llame al finalizador en su método de trabajo como después de ex.Hash call, el CLR sabe que la ex instancia ya no será necesaria ...

Ahora, si quieres mantener viva la instancia, haz esto:

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 hace ... nada :) es un método vacío no inlinable / jittable cuyo único propósito es engañar al GC para que piense que el objeto se usará después de esto.

ADVERTENCIA: su ejemplo es perfectamente válido si el método DoWork fuera un método C ++ administrado ... DO debe mantener las instancias administradas manualmente manualmente si no desea que el destructor esté Llamado desde dentro de otro hilo. ES DECIR. pasa una referencia a un objeto administrado que va a eliminar un blob de memoria no administrada cuando se finalice, y el método utiliza este mismo blob. Si no mantiene viva la instancia, tendrá una condición de carrera entre el GC y el hilo de su método.

Y esto terminará en lágrimas. Y gestionado la corrupción del montón ...

El código completo

A continuación encontrará el código completo, copiado / pegado desde un archivo .cs de Visual C ++ 2008. Como ahora estoy en Linux, y sin ningún compilador Mono ni conocimiento sobre su uso, no hay forma de que pueda hacer pruebas ahora. Aún así, hace un par de horas, vi este código de trabajo y su error:

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

Para aquellos interesados, puedo enviar el proyecto comprimido a través de correo electrónico.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top