c# - funciona - Finalizer lanzado mientras su objeto todavía se estaba utilizando
garbage collector c# como funciona (8)
Resumen: se supone que C # /. NET es basura recolectada. C # tiene un destructor, usado para limpiar recursos. ¿Qué sucede cuando un objeto A es basura recolectada en la misma línea? 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 #, la maestra nos mostró un código que contenía un error solo cuando se ejecutaba en multiprocesadores.
Resumiré para decir que a veces, el compilador o el JIT se arruinan al llamar al finalizador de un objeto de la clase C # antes de volver del método llamado.
El código completo, proporcionado en la documentación de Visual C ++ 2005, se publicará como una "respuesta" para evitar hacer preguntas muy grandes, pero las esenciales están a continuación:
La siguiente clase tiene una propiedad "Hash" que devolverá una copia clonada de una matriz interna. En es 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 la propiedad "Hash" de "Ejemplo", obtendrás una copia limpia de la matriz, cuyo primer elemento sigue siendo 2, ya que el objeto se está utilizando (y, como tal, no se está basura recolectada / finalizada):
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á funcionando dentro de un hilo y, por supuesto, para la prueba, la aplicación tiene muchos subprocesos:
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 de ejecuciones de DoWork, al parecer, el recolector de basura hace su magia, y trata de reclamar "ex", ya que ya no se menciona en el código de remangación de la función, y esta vez es más rápido que el "hash" obtener el método. Entonces, lo que tenemos al final es un clon de una matriz de bytes zero-ed, en lugar de tener la derecha (con el 1er elemento en 2).
Supongo que hay una línea de entrada 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 acceso simple 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 ser considerado como un error del compilador del JIT?
editar
Vea 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 (múltiples ejecutables simultáneos, modo de lanzamiento, etc.)
Interesante comentario 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 {
...
}
}
}
Entonces, no podemos decir por cuánto tiempo ''aC'' podría vivir en el código anterior. El JIT puede informar la referencia hasta que se complete Other.work (). Podría 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 puede considerar que esta tarea es un código muerto y eliminarla. Independientemente de cuándo el JIT deje de informar la referencia, es posible que el GC no consiga recolectarlo durante un tiempo.
Es más interesante preocuparse por el primer punto en que se pueda recolectar aC. Si usted es como la mayoría de las personas, adivinará que el coordinador de control más cercano elegible para la recopilación se encuentra al final de la cláusula "si" de Other.work (), donde agregué el comentario. De hecho, las llaves no existen en la IL. Son un contrato sintáctico entre usted y su compilador de idioma. Other.work () puede dejar de reportar aC tan pronto como haya iniciado la llamada a aC.m ().
El código completo
A continuación, encontrará el código completo, copiar / pegar desde un archivo .cs de Visual C ++ 2008. Como ahora estoy en Linux y sin ningún compilador de Mono ni conocimiento sobre su uso, no hay manera de que pueda hacer pruebas ahora. Aún así, hace un par de horas, vi este código funcionar 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 por correo electrónico.
Creo que lo que está viendo es un comportamiento razonable debido a que las cosas se están ejecutando en varios hilos. Esta es la razón del método GC.KeepAlive (), que se debe usar en este caso para indicar al GC que el objeto todavía se está utilizando y que no es candidato para la limpieza.
Al observar la función DoWork en su respuesta de "código completo", 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 es elegible para la recolección de basura en ese punto. Agregar la llamada a GC.KeepAlive evitaría que esto sucediera.
Es perfectamente normal que se llame al finalizador en su método de trabajo, ya que después de la llamada exHash, el CLR sabe que la instancia ex 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 no ... nada :) es un método vacío ineludible / 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 fue un método administrado de C ++ ... DEBE mantener manualmente activas las instancias administradas si no desea que se invoque el destructor desde otro subproceso. ES DECIR. pasa una referencia a un objeto administrado que va a eliminar un blob de memoria no administrada cuando se finaliza, y el método está usando 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 administrado la corrupción del montón ...
Es simplemente un error en su código: los finalizadores no deberían acceder a los objetos administrados.
La única razón para implementar un finalizador es liberar recursos no administrados. Y en este caso, debe implementar cuidadosamente el patrón estándar IDisposable .
Con este patrón, implementa un método protegido "Dispose protegido (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 recursos no administrados, por lo que no debería implementar un finalizador.
Lo que estás viendo es perfectamente natural.
No mantiene una referencia al objeto que posee la matriz de bytes, por lo que ese objeto (no la matriz de bytes) es realmente gratuito para que el recolector de elementos no utilizados lo recopile.
El recolector de basura realmente puede ser tan agresivo.
Por lo tanto, 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 confunde esa estructura de datos, también necesita mantener una referencia en vivo del objeto.
El recolector de basura ve que la variable ex ya no se utiliza en ese método, por lo que puede hacerlo, y como usted notará, la basura lo recogerá en las circunstancias correctas (es decir, el tiempo y la necesidad).
La forma correcta de hacerlo es llamar a GC.KeepAlive en 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 Applied .NET Framework Programming de Jeffrey Richter.
Sí, este es un problema que ha surgido antes .
Es aún más divertido en el sentido de que necesitas ejecutar la liberación para que esto suceda y terminas estrangulando tu cabeza diciendo ''eh, ¿cómo puede ser eso nulo?''.
esto parece una condición de carrera entre su hilo de trabajo y el (los) hilo (s) de GC; para evitarlo, creo que hay dos opciones:
(1) cambie su instrucción if para usar ex.Hash [0] en lugar de res, para que ex no pueda ser GC''d prematuramente, o
(2) bloqueo ex durante la duración de la llamada a Hash
ese es un ejemplo bastante interesante: ¿el punto del profesor fue 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?