¿Cuándo usar referencias débiles en .Net?

56

Personalmente no me he encontrado con una situación en la que haya necesitado usar el tipo WeakReference en .Net, pero la creencia popular parece ser que debería usarse en cachés. El Dr. Jon Harrop dio un muy buen caso contra el uso de WeakReferences en cachés en su respuesta a esta pregunta.

También he escuchado a menudo que los desarrolladores de AS3 hablan sobre el uso de referencias débiles para ahorrar en la huella de la memoria, pero según las conversaciones que he tenido, parece agregar complejidad sin lograr necesariamente el objetivo previsto, y el comportamiento en tiempo de ejecución es bastante impredecible. Tanto es así que muchos simplemente se dan por vencidos y, en cambio, administran el uso de la memoria con más cuidado / optimizan su código para que consuman menos memoria (o para compensar más ciclos de CPU y menor huella de memoria).

El Dr. Jon Harrop también señaló en su respuesta que las referencias débiles .Net no son suaves, y hay una colección agresiva de referencias débiles en gen0. Según MSDN , ¡las referencias largas y débiles le brindan el potencial de recrear un objeto but the state of the object remains unpredictable.!

Dadas estas características, no puedo pensar en una situación en la que las referencias débiles serían útiles, ¿tal vez alguien podría iluminarme?

themon ardiente
fuente
3
Ya ha descrito los usos potenciales para ello. Por supuesto, hay otras formas de abordar estas situaciones, pero hay más de una forma de desollar a un gato. Si está buscando una prueba de balas "siempre debe usar una referencia débil cuando X", dudo que encuentre una.
2
@ itsme86: no estoy buscando un caso de uso a prueba de balas, solo aquellos para los que las referencias débiles son adecuadas y tienen sentido. El caso de uso de caché por ejemplo, porque las referencias débiles son tan ansiosamente recogen sólo va a causar más errores de caché incluso cuando usted tiene un montón de memoria disponible
44
Estoy un poco decepcionado de que haya muchos votos para cerrar. No me importaría ver una respuesta o alguna discusión sobre esto (en b4 "Stack Overflow no es un foro").
ta.speot.es el
@theburningmonk Ese es el desplazamiento a cambio de la ganancia de memoria. En el marco actual, es dudoso que alguien recurra directamente a la herramienta WeakReference incluso al implementar una memoria caché, ya que existen sistemas integrales de almacenamiento en caché fácilmente disponibles.
Aquí hay un ejemplo vergonzosamente demasiado complicado de usarlos (para el Patrón de eventos débiles que ta.speot.is describe a continuación)
Benjol

Respuestas:

39

He encontrado aplicaciones prácticas legítimas de referencias débiles en los siguientes tres escenarios del mundo real que realmente me sucedieron personalmente:

Aplicación 1: controladores de eventos

Eres emprendedor Su empresa vende un control de líneas de chispa para WPF. Las ventas son excelentes, pero los costos de soporte lo están matando. Demasiados clientes se quejan de la acumulación de CPU y las pérdidas de memoria cuando se desplazan a través de pantallas llenas de líneas de chispa. El problema es que su aplicación está creando nuevas líneas de chispa a medida que aparecen, pero el enlace de datos está evitando que las viejas se recojan. ¿Qué haces?

Introduzca una referencia débil entre el enlace de datos y su control para que el enlace de datos solo ya no evite que su control se recolecte basura. Luego, agregue un finalizador a su control que elimine el enlace de datos cuando se recopile.

Aplicación 2: gráficos mutables

Eres el próximo John Carmack. Has inventado una nueva representación ingeniosa basada en gráficos de superficies de subdivisión jerárquica que hace que los juegos de Tim Sweeney parezcan una Nintendo Wii. Obviamente, no voy a decir exactamente cómo funciona, pero todo se centra en este gráfico mutable donde los vecinos de un vértice se pueden encontrar en a Dictionary<Vertex, SortedSet<Vertex>>. La topología del gráfico sigue cambiando a medida que el jugador corre. Solo hay un problema: su estructura de datos está eliminando subgráficos inalcanzables a medida que se ejecuta y necesita eliminarlos o perderá memoria. Afortunadamente, eres un genio, así que sabes que hay una clase de algoritmos diseñados específicamente para localizar y recolectar subgrafías inalcanzables: ¡recolectores de basura! Uno lee excelente monografía Richard Jones' sobre el temapero te deja perplejo y preocupado por tu inminente plazo. ¿Qué haces?

¡Simplemente reemplazando su Dictionarycon una tabla de hash débil, puede aprovechar el GC existente y hacer que recopile automáticamente sus subgrafos inalcanzables para usted! Volver a hojear los anuncios de Ferrari.

Aplicación 3: Decorar árboles

Estás colgado del techo de una habitación cilíndrica con un teclado. Tienes 60 segundos para examinar BIG DATA antes de que alguien te encuentre. Llegaste preparado con un hermoso analizador basado en secuencias que se basa en el GC para recolectar fragmentos de AST después de haber sido analizados. Pero se da cuenta de que necesita metadatos adicionales en cada AST Nodey que los necesita rápidamente. ¿Qué haces?

Puede usar a Dictionary<Node, Metadata>para asociar metadatos con cada nodo, pero, a menos que lo borre, las fuertes referencias del diccionario a los nodos AST antiguos los mantendrán vivos y perderán memoria. La solución es una tabla hash débil que mantiene solo referencias débiles a las claves y la basura recolecta enlaces de valores clave cuando la clave se vuelve inalcanzable. Luego, a medida que los nodos AST se vuelven inalcanzables, se recogen basura y su enlace clave-valor se elimina del diccionario, dejando los metadatos correspondientes inalcanzables para que también se recopilen. Luego, todo lo que tiene que hacer después de que su circuito principal haya terminado es deslizarse hacia arriba a través del respiradero recordando reemplazarlo justo cuando entra el guardia de seguridad.

Tenga en cuenta que en las tres aplicaciones del mundo real que realmente me sucedieron, quería que el GC se recopilara de la forma más agresiva posible. Es por eso que estas son aplicaciones legítimas. Todos los demás están equivocados.

Jon Harrop
fuente
2
Las referencias débiles no funcionarán para la aplicación 2 si las subgrafías inaccesibles contienen ciclos. Esto se debe a que una tabla hash débil generalmente tiene referencias débiles a las claves, pero referencias fuertes a los valores. Necesitaría una tabla hash que mantenga fuertes referencias a los valores solo mientras la clave aún sea accesible -> ver ephemerons ( ConditionalWeakTableen .NET).
Daniel
@Daniel ¿No se supone que el GC puede manejar ciclos inalcanzables? ¿Cómo sería esto no se recogerá cuando un ciclo inalcanzable de referencias fuertes podría ser recogida?
binki
Oh, creo que ya veo. Supuse que eso ConditionalWeakTablees lo que usarían las aplicaciones 2 y 3, mientras que algunas personas en otras publicaciones realmente usan Dictionary<WeakReference, T>. No tengo idea de por qué: siempre terminarías con una tonelada de WeakReferencevalores nulos con valores a los que ninguna tecla puede acceder independientemente de cómo lo hagas. Ridik
binki
@binki: "¿No se supone que el GC puede manejar ciclos inalcanzables? ¿Cómo no se recolectaría esto cuando se recolectaría un ciclo inalcanzable de referencias fuertes?". Tiene un diccionario codificado en objetos únicos que no se pueden recrear. Cuando uno de sus objetos clave se vuelve inalcanzable, se puede recolectar basura, pero el valor correspondiente en el diccionario ni siquiera se pensará que es teóricamente inalcanzable porque un diccionario ordinario tendrá una fuerte referencia a él, manteniéndolo vivo. Entonces usas un diccionario débil.
Jon Harrop
@Daniel: "Las referencias débiles no funcionarán para la aplicación 2 si los subgráficos inalcanzables contienen ciclos. Esto se debe a que una tabla hash débil generalmente tiene referencias débiles a las claves, pero fuertes referencias a los valores. Necesitarías una tabla hash que mantiene fuertes referencias a los valores solo mientras la clave todavía está accesible ". Si. Probablemente sea mejor codificar el gráfico directamente con bordes como punteros para que el GC lo recoja solo.
Jon Harrop
19

Dadas estas características, no puedo pensar en una situación en la que las referencias débiles serían útiles, ¿tal vez alguien podría iluminarme?

Documento de Microsoft Patrones de eventos débiles .

En las aplicaciones, es posible que los controladores que están conectados a las fuentes de eventos no se destruyan en coordinación con el objeto de escucha que adjuntó el controlador a la fuente. Esta situación puede provocar pérdidas de memoria. Windows Presentation Foundation (WPF) presenta un patrón de diseño que puede usarse para abordar este problema, proporcionando una clase de administrador dedicada para eventos particulares e implementando una interfaz en los oyentes para ese evento. Este patrón de diseño se conoce como el patrón de evento débil.

...

El patrón de evento débil está diseñado para resolver este problema de pérdida de memoria. El patrón de evento débil se puede usar cuando un oyente necesita registrarse para un evento, pero el oyente no sabe explícitamente cuándo cancelar el registro. El patrón de evento débil también se puede usar siempre que la vida útil del objeto de la fuente exceda la vida útil útil del objeto del oyente. (En este caso, útil lo determina usted.) El patrón de evento débil permite que el oyente se registre y reciba el evento sin afectar de ninguna manera las características de vida útil del objeto del oyente. En efecto, la referencia implícita de la fuente no determina si el oyente es elegible para la recolección de basura. La referencia es una referencia débil, por lo tanto, el nombre del patrón de evento débil y las API relacionadas. El oyente puede recolectarse basura o destruirse de otra manera, y la fuente puede continuar sin retener referencias de manejador no coleccionables a un objeto ahora destruido.

ta.speot.is
fuente
Esa URL selecciona automáticamente la versión más reciente de .NET (4.5 actualmente) en la que "este tema ya no está disponible". Seleccionar .NET 4.0 en su lugar funciona ( msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx )
maxp
13

Déjame poner esto primero y volver a ello:

Una referencia débil es útil cuando desea mantener pestañas en un objeto, pero NO desea que sus observaciones eviten que se recopile ese objeto

Entonces, comencemos desde el principio:

- Disculpas de antemano por cualquier ofensa no intencional, pero voy a retroceder al nivel de "Dick y Jane" por un momento ya que uno nunca puede decirle a la audiencia.

Entonces, cuando tienes un objeto X, especificémoslo como una instancia de class Foo, NO PUEDE vivir solo (principalmente cierto); De la misma manera que "Ningún hombre es una isla", solo hay unas pocas formas en que un objeto puede promoverse a Islandhood, aunque se llama ser una raíz GC en CLR. Ser una Raíz GC, o tener una cadena establecida de conexiones / referencias a una raíz GC, es básicamente lo que determina si Foo x = new Foo()se recolecta o no la basura.

Si no puede caminar de regreso a alguna raíz del GC, ya sea caminando por el montón o apilando, queda efectivamente huérfano y probablemente será marcado / recogido el próximo ciclo.

En este punto, veamos algunos ejemplos horriblemente inventados:

Primero, nuestro Foo:

public class Foo 
{
    private static volatile int _ref = 0;
    public event EventHandler FooEvent;
    public Foo()
    {
        _ref++;
        Console.WriteLine("I am #{0}", _ref);
    }
    ~Foo()
    {
        Console.WriteLine("#{0} dying!", _ref--);
    }
}

Bastante simple: no es seguro para subprocesos, así que no intente eso, pero mantiene un "recuento de referencias" aproximado de instancias activas y decrementos cuando se finalizan.

Ahora echemos un vistazo a FooConsumer:

public class NastySingleton
{
    // Static member status is one way to "get promoted" to a GC root...
    private static NastySingleton _instance = new NastySingleton();
    public static NastySingleton Instance { get { return _instance;} }

    // testing out "Hard references"
    private Dictionary<Foo, int> _counter = new Dictionary<Foo,int>();
    // testing out "Weak references"
    private Dictionary<WeakReference, int> _weakCounter = new Dictionary<WeakReference,int>();

    // Creates a strong link to Foo instance
    public void ListenToThisFoo(Foo foo)
    {
        _counter[foo] = 0;
        foo.FooEvent += (o, e) => _counter[foo]++;
    }

    // Creates a weak link to Foo instance
    public void ListenToThisFooWeakly(Foo foo)
    {
        WeakReference fooRef = new WeakReference(foo);
        _weakCounter[fooRef] = 0;
        foo.FooEvent += (o, e) => _weakCounter[fooRef]++;
    }

    private void HandleEvent(object sender, EventArgs args, Foo originalfoo)
    {
        Console.WriteLine("Derp");
    }
}

Entonces, tenemos un objeto que ya es una raíz de GC propia (bueno ... para ser específicos, se enraizará a través de una cadena directamente al dominio de la aplicación que ejecuta esta aplicación, pero ese es otro tema) que tiene dos métodos de engancharse a una Fooinstancia - probémoslo:

// Our foo
var f = new Foo();

// Create a "hard reference"
NastySingleton.Instance.ListenToThisFoo(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Ahora, a partir de lo anterior, ¿esperaría que el objeto al que se hizo referencia alguna vez ffuera "coleccionable"?

No, porque ahora hay otro objeto que tiene una referencia a él, el Dictionaryde esa Singletoninstancia estática.

Ok, intentemos con el enfoque débil:

f = new Foo();
NastySingleton.Instance.ListenToThisFooWeakly(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
// This should collect # 2 - you'll see a "#2 dying"
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Ahora, cuando criticamos nuestra referencia a lo Fooque fue una vez f, no hay más referencias "duras" al objeto, por lo que es coleccionable: lo WeakReferencecreado por el oyente débil no lo impedirá.

Buenos casos de uso:

  • Controladores de eventos (aunque lea esto primero: Eventos débiles en C # )

  • Tienes una situación en la que causarías una "referencia recursiva" (es decir, el objeto A se refiere al objeto B, que se refiere al objeto A, también conocido como "Fuga de memoria") (editar: derp, por supuesto, esto no es no es cierto)

  • Desea "transmitir" algo a una colección de objetos, pero no quiere ser lo que los mantenga vivos; a List<WeakReference>puede mantenerse fácilmente e incluso podarse quitando donderef.Target == null

JerKimball
fuente
1
Con respecto a su segundo caso de uso, el recolector de basura maneja muy bien las referencias circulares. "el objeto A se refiere al objeto B, que se refiere al objeto A" definitivamente no es una pérdida de memoria.
Joe Daley
@JoeDaley, estoy de acuerdo. .NET GC utiliza un algoritmo de marcado y barrido que (creo que recuerdo esto correctamente) marca todos los objetos para la colección y luego sigue las referencias de "raíces" (referencias de objetos en la pila, objetos estáticos), desmarcando objetos para la colección . Si existe una referencia circular pero no se puede acceder a ninguno de los objetos desde una raíz, los objetos no están sin marcar para la colección y, por lo tanto, son elegibles para la colección.
ta.speot.es el
1
@JoeDaley: los dos, por supuesto, están en lo correcto, lo apresuraron hacia el final ... lo editaré.
JerKimball
4

ingrese la descripción de la imagen aquí

Al igual que las filtraciones lógicas que son realmente difíciles de rastrear, mientras que los usuarios tienden a notar que ejecutar su software durante mucho tiempo tiende a ocupar más y más memoria y se vuelve más y más lento hasta que se reinicia. Yo no.

Tenga en cuenta lo que sucede si, cuando el usuario solicita eliminar el recurso de la aplicación anterior, Thing2no puede manejar adecuadamente dicho evento en:

  1. Punteros
  2. Referencias fuertes
  3. Referencias débiles

... y bajo cuál de estos errores probablemente quedaría atrapado durante las pruebas, y cuál no y volaría bajo el radar como un insecto de caza furtivo. La propiedad compartida es, más a menudo que la mayoría, una idea sin sentido.


fuente
1

Un ejemplo muy ilustrativo de referencias débiles utilizadas con buenos resultados es la ConditionalWeakTable , que es utilizada por el DLR (entre otros lugares) para adjuntar "miembros" adicionales a los objetos.

No quieres que la mesa mantenga vivo el objeto. Este concepto simplemente no podría funcionar sin referencias débiles.

Pero me parece que todos los usos de las referencias débiles llegaron mucho después de que se agregaron al lenguaje, ya que las referencias débiles han sido parte de .NET desde la versión 1.1. Simplemente parece algo que le gustaría agregar, de modo que la falta de destrucción determinista no lo arrinconará en lo que respecta a las características del lenguaje.

GregRos
fuente
De hecho, he descubierto que aunque la tabla usa el concepto de referencias débiles, la implementación real no involucra el WeakReferencetipo, ya que la situación es mucho más compleja. Utiliza diferentes funcionalidades expuestas por el CLR.
GregRos
-2

Si tiene implementada la capa de caché con C #, es mucho mejor colocar sus datos en la caché como referencias débiles, podría ayudar a mejorar el rendimiento de la capa de caché.

Piense que ese enfoque también podría aplicarse a la implementación de la sesión. Debido a que la sesión es un objeto de larga vida la mayor parte del tiempo, podría ser algún caso cuando no tiene memoria para un nuevo usuario. En ese caso, será mucho mejor eliminar otro objeto de sesión de usuario y luego lanzar OutOfMemoryException.

Además, si tiene un objeto grande en su aplicación (alguna tabla de búsqueda grande, etc.), eso debería usarse muy raramente y recrear dicho objeto no es un procedimiento muy costoso. Entonces mejor que sea una referencia semanal para tener una manera de liberar tu memoria cuando realmente lo necesites.

Ph0en1x
fuente
55
pero el problema con las referencias débiles (vea la respuesta a la que he hecho referencia) es que están recolectadas con mucho entusiasmo, y la colección no está vinculada a la disponibilidad de espacio en la memoria. Entonces terminas con más errores de caché cuando no hay presión en la memoria.
1
Pero para su segundo punto sobre objetos grandes, el documento de MSDN establece que si bien las referencias largas y débiles le permiten recrear un objeto, su estado sigue siendo impredecible. Si va a recrearlo desde cero cada vez, ¿por qué molestarse en usar una referencia débil cuando solo puede llamar a una función / método para crearlo a pedido y devolver una instancia transitoria?
Hay una situación en la que el almacenamiento en caché es útil: si se crean objetos inmutables con frecuencia, muchos de los cuales serán idénticos (por ejemplo, leer muchas líneas de un archivo que se espera que tenga muchos duplicados), cada cadena se creará como un nuevo objeto , pero si una línea coincide con otra línea a la que ya existe una referencia, la eficiencia de la memoria puede mejorarse si se abandona la nueva instancia y se sustituye una referencia a la instancia preexistente. Tenga en cuenta que esta sustitución es útil porque la otra referencia se mantiene de todos modos. Si no fuera código, debería quedarse con el nuevo.
supercat