weakreference weak-references

weak-references - weakreference android



Referencias débiles-¿Qué tan útiles son? (4)

Así que últimamente he estado reflexionando sobre algunas ideas de administración de memoria automática, específicamente, he estado considerando la implementación de un administrador de memoria basado en el conteo de referencias. Por supuesto, todo el mundo sabe que las referencias circulares matan el ingenuo conteo de referencias. La solución: las referencias débiles. Personalmente, odio usar referencias débiles de esta manera (hay otras formas más intuitivas de tratar esto, a través de la detección de ciclos), pero me hizo pensar: ¿dónde más podría ser útil una referencia débil?

Me imagino que debe haber alguna razón para que existan, especialmente en los idiomas con rastreo de recolección de basura, que no sufren el problema de las referencias cíclicas (C # y Java son los que estoy familiarizado, y Java incluso tiene tres tipos de referencias débiles !). Sin embargo, cuando traté de encontrar algunos casos de uso sólidos para ellos, prácticamente obtuve ideas como "Utilícelas para implementar cachés" (lo he visto varias veces en SO). Tampoco me gusta eso, ya que se basan en el hecho de que un GC de rastreo probablemente no recopilará un objeto inmediatamente después de que ya no tenga una referencia fuerte, excepto en situaciones de poca memoria. Este tipo de casos son absolutamente inválidos con el recuento de referencias GC ya que un objeto se destruye inmediatamente después de que ya no se hace referencia (excepto posiblemente en el caso de ciclos).

Pero eso realmente me deja pensando: ¿Cómo puede ser útil una referencia débil? Si no puede contar con que haga referencia a un objeto, y no es necesario para cosas como ciclos de ruptura, ¿por qué usar uno?


Pero eso realmente me deja pensando: ¿Cómo puede ser útil una referencia débil? Si no puede contar con que haga referencia a un objeto, y no es necesario para cosas como ciclos de ruptura, ¿por qué usar uno?

Tengo una opinión ciertamente dogmática de que las referencias débiles deberían ser la manera predeterminada de almacenar de forma persistente una referencia a un objeto con referencias sólidas que requieren una sintaxis más explícita, como por ejemplo:

class Foo { ... // Stores a weak reference to bar. ''Foo'' does not // own bar. private Bar bar; // Stores a strong reference to ''Baz''. ''Foo'' does // own Baz. private strong Baz baz; }

... mientras tanto, lo contrario para los locales dentro de una función / método:

void some_function() { // Stores a strong reference to ''Bar''. It will // not be destroyed until it goes out of scope. Bar bar = ...; // Stores a weak reference to ''Baz''. It can be // destroyed before the weak reference goes out // of scope. weak Baz baz_weak = ...; ... // Acquire a strong reference to ''Baz''. Baz baz = baz_weak; if (baz) { // If ''baz'' has not been destroyed, // do something with it. baz.do_something(); } }

Historia de horror

Para entender por qué tengo esta firme opinión y por qué las referencias débiles son útiles, solo compartiré una historia personal según mi experiencia en una antigua compañía que abarcó GC en todos los ámbitos.

Esto era para un producto 3D que trataba con cosas pesadas como mallas y texturas, algunas de las cuales podían abarcar más de un gigabyte en la memoria. El software giraba en torno a un gráfico de escena y una arquitectura de complementos donde cualquier complemento podía acceder al gráfico de la escena y a los elementos internos, como texturas, mallas, luces o cámaras.

Ahora, lo que sucedió fue que el equipo y nuestros desarrolladores externos no estaban tan familiarizados con las referencias débiles, por lo que teníamos personas que almacenaban referencias de objetos a cosas en el gráfico de la escena de izquierda a derecha. Los complementos de cámara almacenan una lista de referencias de objetos fuertes para excluir de la vista de cámara. El renderizador almacenaría una lista de objetos para usar al renderizar como una lista de referencias claras. Las luces actuarían de manera similar a las cámaras y tendrían listas de exclusión / inclusión. Los complementos de Shader almacenan referencias a las texturas que utilizan. La lista sigue y sigue.

En realidad, yo era el que tenía que hacer una presentación sobre la importancia de las referencias débiles para nuestro equipo después de un año de desarrollo después de descubrir tantas fugas, aunque no fui yo quien impulsó la decisión de diseño de usar GC (en realidad era contra esto). También tuve que implementar el soporte para referencias débiles en nuestro recolector de basura propietario después de la presentación porque nuestro recolector de basura (escrito por otra persona) ni siquiera admitía las referencias débiles originalmente.

Fugas logicas

Y, efectivamente, terminamos con un software en el que, cuando un usuario deseaba eliminar un objeto de la escena como una malla o textura, en lugar de liberar esa memoria, la aplicación seguía usando la memoria porque algo, en algún lugar a lo largo del gran espacio. codebase, aún mantenía una referencia a esos objetos de escena y no los dejaba ir cuando el usuario lo solicitaba explícitamente. Incluso después de despejar la escena, el software podría tomar 3 gigabytes de memoria y aún más cuanto más tiempo lo uses. Y todo esto se debió a que el código base, incluidos los desarrolladores externos, no pudo usar referencias débiles cuando fue apropiado.

Como resultado, cuando el usuario solicitó eliminar una malla de una escena, tal vez 9/10 lugares donde se almacenaron las referencias a una malla dada liberarían adecuadamente la referencia, configurándola en una referencia nula o eliminando la referencia de una lista para permitir que recolector de basura para recogerlo. Sin embargo, a menudo habría un décimo lugar en el que se olvidó de manejar tal evento, manteniendo la malla en la memoria hasta que esa cosa en sí también se eliminara de la escena (y algunas veces esas cosas vivían fuera de la escena y se almacenaban en la raíz de la aplicación) . Y esa cascada a veces hasta el punto en que el software simplemente consumiría más y más memoria cuanto más lo usara hasta el punto en que los complementos del controlador (que se quedan incluso después de borrar una escena) prolongarían la vida útil de toda la escena al almacenar un una referencia inyectada a la raíz de la escena para DI, momento en el que la memoria no se liberaría incluso después de borrar toda la escena, lo que obliga a los usuarios a reiniciar periódicamente el software cada una o dos horas para volver a una cantidad razonable de uso de memoria .

Estos no eran errores fáciles de descubrir. Todo lo que pudimos ver es que la aplicación utilizaba más y más memoria a medida que la ejecutaba. No era algo que pudiéramos reproducir fácilmente en pruebas de unidad o integración de corta duración. Y a veces, después de horas de investigación exhaustiva, descubrimos que ni siquiera nuestro propio código causó estas pérdidas de memoria. Fue dentro de un complemento de terceros que los usuarios usaron a menudo donde el complemento simplemente terminó almacenando una referencia a algo como una malla o textura que no se liberó en respuesta a un evento de eliminación de escena.

Y esa tendencia a perder más y más memoria tiende a estar presente en software escrito en lenguajes recolectados en la basura donde los programadores no tienen cuidado de usar referencias débiles cuando es apropiado. Las referencias débiles deben usarse idealmente en todos los casos en que un objeto no posee otro. Debería haber muchos más casos en los que tenga sentido que referencias fuertes. No tiene sentido para cada objeto que hace referencia a todo para compartir la propiedad en todo. Para la mayoría de los programas, el diseño más sensato es que una cosa en el sistema sea propietaria de otra, como "los gráficos de escena poseen objetos de escena" , no "las cámaras también poseen mallas porque se refieren a ellas en la lista de exclusión de cámaras" .

¡De miedo!

Ahora, GC es muy aterrador en un software a gran escala y de rendimiento crítico donde tales fugas lógicas pueden hacer que la aplicación tome cientos de gigabytes más de memoria de la que debería durante un largo período de tiempo, al tiempo que se ralentiza a un rastreo cuanto más tiempo se ejecuta. , comenzando rápido y luego volviéndose cada vez más lento hasta que lo reinicies.

Cuando intenta investigar el origen de todas estas fugas, podría estar viendo 20 millones de líneas de código, incluidas más fuera de su control, escritas por desarrolladores de complementos, y cualquiera de esas líneas podría prolongar de forma silenciosa la vida útil de un objetar mucho más tiempo de lo apropiado, simplemente almacenando una referencia de objeto a él y no liberarlo en respuesta a los eventos apropiados. Peor aún, todo esto vuela bajo el radar de control de calidad y pruebas automatizadas.

Ese es un escenario de pesadilla en ese contexto y la única manera razonable que veo para evitar tal escenario es tener un estándar de codificación que se base en gran medida en referencias débiles si está usando GC o simplemente evite usar GC en primer lugar.

Fugas de GC

Ahora nunca he tenido la opinión más positiva acerca de la recolección de basura, y es porque en mi campo al menos, no es necesariamente más deseable tener una fuga de recursos lógicos que vuela bajo el radar de pruebas sobre, digamos, un puntero colgante Accidente que puede ser detectado y reproducido fácilmente, y muy probablemente corregido por el desarrollador antes de que incluso confirme su código si hay una prueba de sonido y un procedimiento de IC.

En mi caso particular, los errores más deseables si elegimos entre los males son los más fáciles de descubrir y reproducir, y las fugas de recursos de tipo GC no son fáciles de descubrir y no son fáciles de reproducir en ningún sentido que te ayude Descubre la fuente de esa fuga.

Sin embargo, mi opinión sobre GC se vuelve mucho más favorable entre los equipos y las bases de código que hacen un uso intensivo de las referencias débiles y solo usan referencias sólidas donde tiene sentido real desde un punto de vista de diseño de alto nivel para extender la vida útil de un objeto.

GC no es una defensa práctica contra las fugas de memoria, todo lo contrario. Si lo fuera, las aplicaciones con menos fugas en el mundo se escribirían en lenguajes compatibles con GC como Flash, Java, JavaScript, C #, y el software más grande que se pudiera imaginar se escribiría en lenguajes con la administración de memoria más manual como C, momento en el que El kernel de Linux debería ser un infierno de un sistema operativo con fugas que requeriría reiniciarse cada una o dos horas para reducir el uso de la memoria. Pero ese no es el caso. A menudo es todo lo contrario con las aplicaciones más importantes escritas contra GC, y eso es porque GC en realidad tiende a hacer que sea más difícil evitar las fugas lógicas. Lo que sí ayuda es evitar las fugas físicas (pero las fugas físicas son lo suficientemente fáciles de detectar y, en primer lugar, sin importar el idioma que use), y lo que sí ayuda es evitar colisiones con punteros en software de misión crítica donde es más deseable. perder memoria que bloquearse porque la vida de una persona está en juego o porque una falla podría traducirse en que un servidor no esté disponible durante horas y horas. No trabajo en dominios de misión crítica; Trabajo en rendimiento y en memoria crítica con procesadores de datos épicos que se procesan con cada fotograma único que se representa.

Después de todo, todo lo que tenemos que hacer para crear una fuga lógica con GC es esto:

class Foo { // This makes ''Foo'' instances cause ''bar'' to leak, preventing // it from being destroyed until the ''Foo'' instances are also // destroyed unless the ''Foo'' instances set this to a null // reference at the right time (ex: when the user requests // to remove whatever Bar is from the software). private Bar bar; }

... pero las referencias débiles no arriesgan este problema Cuando estás viendo millones de LOC como el anterior por un lado y fugas de memoria épicas en el otro, es un escenario de pesadilla cuando tienes que investigar qué analógico Foo no pudo establecer la Bar analógica en una referencia nula en el lugar adecuado. tiempo porque esta es la parte que da tanto miedo: el código funciona bien siempre y cuando ignore los gigabytes de pérdida de memoria. Nada dispara ningún tipo de error / excepción, falla de aserción, etc. Nada se bloquea. Toda la unidad y la integración pasan sin queja. Todo funciona, excepto que está perdiendo gigabytes de memoria, lo que provoca que las quejas de los usuarios se desvíen, mientras que todo el equipo se da cuenta de qué partes de la base de código tienen pérdidas y cuáles no, mientras que el control de calidad intenta hacer el control de daños sugiriendo pragmáticamente que los usuarios ahorre su trabajo y reinicie el software cada media hora como si supusiera que se trata de algún tipo de solución.

Las referencias débiles ayudan mucho

Entonces, por favor, haga uso de referencias débiles cuando sea apropiado, y cuando corresponda, quiero decir cuando no tiene sentido que un objeto comparta la propiedad de otro.

Son útiles porque aún puede detectar cuándo un objeto ha sido destruido sin prolongar su vida útil. Las referencias sólidas son útiles cuando realmente necesita extender la vida útil de un objeto, como dentro de un hilo de corta duración para que el objeto no se destruya antes de que el hilo termine de procesarlo, o dentro de un objeto que realmente tenga sentido poseer otro.

Utilizando el ejemplo de mi gráfico de escena, una lista de exclusión de cámaras no necesita poseer objetos de escena que ya pertenecen al gráfico de escena. Lógicamente eso no tiene sentido si lo hiciera. Si estamos en el tablero de dibujo, nadie debería pensar: "sí, las cámaras también deben poseer objetos de escena además del gráfico de escena en sí".

Solo necesita esas referencias para poder referirse fácilmente a esos elementos. Cuando lo hace, puede adquirir fuertes referencias a ellos a partir de las referencias débiles almacenadas antes de procesarlos y también verificar si el usuario las ha eliminado antes de hacerlo, en lugar de extender su vida útil posiblemente al punto donde la memoria se pierde hasta que la cámara también se elimina.

Si la cámara desea utilizar un tipo de implementación perezosa conveniente que no tenga que preocuparse por los eventos de eliminación de escenas, las referencias débiles al menos le permiten hacerlo sin perder cantidades épicas de memoria por todas partes. Las referencias débiles aún le permiten descubrir, en retrospectiva, cuándo se han eliminado objetos de la escena y tal vez eliminar las referencias débiles destruidas de la lista sin molestar a los eventos de eliminación de escenas. La solución ideal para mí es utilizar tanto las referencias débiles como los eventos de eliminación de escenas, pero al menos la lista de exclusión de la cámara debería usar referencias débiles, no referencias fuertes.

La utilidad de las referencias débiles en un ambiente de equipo

Y eso llega al corazón de la utilidad de las referencias débiles para mí. Nunca son absolutamente necesarios si todos los desarrolladores de su equipo eliminan / anulan a fondo las referencias de objetos en los momentos apropiados en respuesta a los eventos apropiados. Pero al menos en los equipos grandes, los errores que pueden ocurrir y que no se pueden evitar de manera absoluta por los estándares de ingeniería a menudo terminan ocurriendo, y en ocasiones a tasas asombrosas. Y las referencias débiles son una defensa fantástica contra la tendencia de las aplicaciones que giran alrededor de GC a tener fugas lógicas cuanto más se ejecutan. En mi opinión, son un mecanismo defensivo para ayudar a traducir errores que se manifestarían en forma de pérdidas de memoria difíciles de detectar en usos fáciles de detectar de una referencia no válida a un objeto ya destruido.

La seguridad

Puede que no parezcan tan útiles en el mismo sentido que un programador de ensamblajes no encuentre mucho uso para la seguridad de tipos. Después de todo, puede hacer todo lo que necesita con bits y bytes sin procesar y las instrucciones de ensamblaje adecuadas. Sin embargo, la seguridad de tipos ayuda a detectar errores humanos más fácilmente al hacer que los desarrolladores humanos expresen más explícitamente lo que quieren hacer y limitan lo que pueden hacer con un tipo en particular. Veo referencias débiles en un sentido similar. Ayudan a detectar errores humanos que de otro modo habrían conducido a fugas de recursos si no se usaran referencias débiles. Se impone deliberadamente restricciones sobre usted mismo como, "De acuerdo, esta es una referencia débil a un objeto, por lo que posiblemente no puede extender su vida útil y causar una fuga lógica", lo cual es inconveniente, pero también lo es la seguridad de tipo para un programador de ensamblajes. Todavía puede ayudar a prevenir algunos errores muy desagradables.

Son una característica de seguridad del idioma si me preguntas y, como cualquier otra característica de seguridad, no es absolutamente necesaria y, por lo general, no la apreciarás hasta que encuentres a un equipo que tropieza con las mismas cosas una y otra vez porque esa característica de seguridad no existía. no se utiliza adecuadamente. Para los desarrolladores en solitario, la seguridad es a menudo una de las cosas más fáciles de ignorar, ya que si eres competente y cuidadoso, es posible que no lo necesites personalmente. Pero multiplique el riesgo de insectos por un equipo completo de personas con habilidades mixtas, y las características de seguridad pueden convertirse en cosas que usted necesita desesperadamente, mientras que las personas comienzan a deslizarse analógicamente en un piso húmedo a la izquierda y a la derecha que evita cuidadosamente todos los días, lo que hace que los cadáveres se acumulen a tu alrededor En cambio, he encontrado con equipos grandes que si no tiene un estándar de codificación que sea fácil de seguir pero que establezca prácticas de ingeniería seguras con un puño de hierro que, en solo un mes, podría haber acumulado más de cien mil líneas de extremadamente código defectuoso con errores ocultos y difíciles de detectar, como las fugas lógicas de GC mencionadas anteriormente. La cantidad de código roto que se puede acumular en solo un mes sin un estándar para prevenir los errores comunes es bastante asombrosa.

De todos modos, soy cierto que soy un poco dogmático sobre este tema, pero la opinión se formó sobre una gran cantidad de fugas de memoria épicas, por lo que la única respuesta que vi fue simplemente decirles a los desarrolladores: "¡Tengan más cuidado! Ustedes están perdiendo la memoria como ¡loca!" fue hacer que usaran referencias débiles con mayor frecuencia, momento en el que cualquier descuido no se traduciría en cantidades épicas de memoria filtrada. En realidad, llegamos al punto en que descubrimos tantos lugares con fugas en retrospectiva que volaron bajo el radar de las pruebas que deliberadamente rompí la compatibilidad de fuente (aunque no la compatibilidad binaria) en nuestro SDK. Solíamos tener una convención como esta:

typedef Strong<Mesh> MeshRef; typedef Weak<Mesh> MeshWeakRef;

... este fue un GC propietario implementado en C ++ que se ejecuta en un subproceso separado. Lo cambié a esto:

typedef Weak<Mesh> MeshRef; typedef Strong<Mesh> MeshStrongRef;

... y ese simple cambio en la sintaxis y la convención de nombres ayudó enormemente a prevenir más fugas, excepto que lo hicimos un par de años demasiado tarde, haciendo que el control de daños sea más que cualquier otra cosa.


A menudo utilizo WeakReference junto con ThreadLocal o InheritableThreadLocal . Si queremos que un valor sea accesible a una cantidad de subprocesos mientras sea significativo, pero luego elimine el valor de esos subprocesos, no podemos liberar la memoria nosotros mismos porque no hay manera de manipular un valor ThreadLocal en una Hilo distinto al actual. Sin embargo, lo que puede hacer es poner el valor en una WeakReference en esos otros subprocesos (cuando se crea el valor, esto supone que la misma instancia se comparte entre varios subprocesos; tenga en cuenta que esto solo es significativo cuando solo un subconjunto de subprocesos debería tener acceso a este valor o simplemente usaría estáticas) y almacenaría una referencia ThreadLocal en otro ThreadLocal para algún subproceso de trabajo que va a eliminar el valor. Luego, cuando el valor deja de ser significativo, puede solicitar al subproceso de trabajo que elimine la referencia definitiva, lo que hace que los valores de todos los demás subprocesos se coloquen en la cola inmediatamente para la recolección de basura (aunque es posible que no se recojan de inmediato, por lo que vale la pena tener alguna otra manera de evitar que se acceda al valor).


El objeto referenciado por WeakReference será accesible antes del proceso de gc.

Entonces, si queremos tener la información del objeto mientras exista, podemos usar WeakReference. Por ejemplo, el depurador y el optimizador a menudo necesitarán tener la información de un objeto pero no quieren afectar el proceso de GC.

Por cierto, SoftReference es diferente de WeakReference porque el objeto relacionado se recopilará solo cuando la memoria no sea suficiente. Por lo tanto, SoftReference se utilizará para crear un caché global por lo general.


Los controladores de eventos son un buen caso de uso para referencias débiles. El objeto que desencadena eventos necesita una referencia a los objetos para invocar a los controladores de eventos, pero por lo general no desea que se mantenga la referencia del productor del evento para evitar que los consumidores de eventos sean sometidos a GC. Más bien, querría que el productor del evento tuviera una referencia débil, y luego sería responsable de verificar si el objeto al que se hacía referencia todavía estaba presente.