¿Por qué y cómo evitar las pérdidas de memoria del controlador de eventos?

154

Acabo de darme cuenta, al leer algunas preguntas y respuestas en StackOverflow, que agregar controladores de eventos usando +=C # (o supongo, otros lenguajes .net) puede causar pérdidas de memoria comunes ...

He usado controladores de eventos como este en el pasado muchas veces, y nunca me di cuenta de que pueden causar, o han causado, pérdidas de memoria en mis aplicaciones.

¿Cómo funciona esto (es decir, por qué esto realmente causa una pérdida de memoria)?
Como puedo solucionar este problema ? ¿Es -=suficiente usar el mismo controlador de eventos?
¿Existen patrones de diseño comunes o mejores prácticas para manejar situaciones como esta?
Ejemplo: ¿Cómo se supone que debo manejar una aplicación que tiene muchos subprocesos diferentes, usando muchos controladores de eventos diferentes para generar varios eventos en la interfaz de usuario?

¿Hay alguna manera buena y simple de monitorear esto de manera eficiente en una gran aplicación ya construida?

gillyb
fuente

Respuestas:

188

La causa es simple de explicar: mientras se suscribe un controlador de eventos, el editor del evento mantiene una referencia al suscriptor a través del delegado del controlador de eventos (suponiendo que el delegado es un método de instancia).

Si el editor vive más que el suscriptor, mantendrá vivo al suscriptor incluso cuando no haya otras referencias al suscriptor.

Si se da de baja del evento con un controlador igual, entonces sí, eso eliminará el controlador y la posible fuga. Sin embargo, en mi experiencia, esto rara vez es realmente un problema, porque típicamente encuentro que el editor y el suscriptor tienen vidas aproximadamente iguales de todos modos.

Eso es una causa posible ... pero en mi experiencia es bastante exageradas. Su kilometraje puede variar, por supuesto ... solo necesita tener cuidado.

Jon Skeet
fuente
... He visto a algunas personas escribir sobre esto en respuestas a preguntas como "¿cuál es la pérdida de memoria más común en .net"?
gillyb
32
Una forma de evitar esto desde el lado del editor es establecer el evento como nulo una vez que esté seguro de que no lo activará más. Esto eliminará implícitamente a todos los suscriptores y puede ser útil cuando ciertos eventos solo se activan durante ciertas etapas de la vida útil del objeto.
JSB ձոգչ
2
El método Dipose sería un buen momento para establecer el evento en nulo
Davi Fiamenghi
66
@DaviFiamenghi: Bueno, si se está eliminando algo, eso es al menos una indicación probable de que pronto será elegible para la recolección de basura, en ese momento no importa qué suscriptores haya.
Jon Skeet
1
@ BrainSlugs83: "y el patrón de evento típico incluye un remitente de todos modos" - sí, pero ese es el productor del evento . Por lo general, la instancia del suscriptor del evento es relevante y el remitente no lo es. Entonces, sí, si puede suscribirse usando un método estático, esto no es un problema, pero en mi experiencia rara vez es una opción.
Jon Skeet
13

Sí, -=es suficiente. Sin embargo, podría ser bastante difícil hacer un seguimiento de cada evento asignado, nunca. (para más detalles, ver la publicación de Jon). Con respecto al patrón de diseño, eche un vistazo al patrón de evento débil .

Femaref
fuente
1
msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx la versión 4.0 todavía lo tiene.
Femaref
Si sé que un editor vivirá más tiempo que el suscriptor, hago que el suscriptor IDisposabley me dé de baja del evento.
Shimmy Weitzhandler
9

He explicado esta confusión en un blog en https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Trataré de resumirlo aquí para que pueda tener una idea clara.

Referencia significa "Necesidad":

En primer lugar, debe comprender que, si el objeto A tiene una referencia al objeto B, significará que el objeto A necesita el objeto B para funcionar, ¿verdad? Por lo tanto, el recolector de basura no recolectará el objeto B mientras el objeto A esté vivo en la memoria.

Creo que esta parte debería ser obvia para un desarrollador.

+ = Significa, inyectando referencia del objeto del lado derecho al objeto izquierdo:

Pero, la confusión proviene del operador C # + =. Este operador no le dice claramente al desarrollador que, el lado derecho de este operador en realidad está inyectando una referencia al objeto del lado izquierdo.

ingrese la descripción de la imagen aquí

Y al hacerlo, el objeto A piensa que necesita el objeto B, aunque, desde su perspectiva, al objeto A no le importa si el objeto B vive o no. Como el objeto A cree que se necesita el objeto B, el objeto A protege al objeto B del recolector de basura mientras el objeto A esté vivo. Pero, si no desea que se brinde esa protección al objeto del suscriptor del evento, puede decir que se produjo una pérdida de memoria.

ingrese la descripción de la imagen aquí

Puede evitar tal fuga separando el controlador de eventos.

¿Cómo tomar una decisión?

Pero, hay muchos eventos y controladores de eventos en toda su base de código. ¿Significa que debe seguir separando los controladores de eventos en todas partes? La respuesta es No. Si tuviera que hacerlo, su base de código será realmente fea y detallada.

Puede seguir un diagrama de flujo simple para determinar si un controlador de eventos de separación es necesario o no.

ingrese la descripción de la imagen aquí

La mayoría de las veces, puede encontrar que el objeto del suscriptor del evento es tan importante como el objeto del editor del evento y se supone que ambos viven al mismo tiempo.

Ejemplo de un escenario donde no necesita preocuparse

Por ejemplo, un evento de clic de botón de una ventana.

ingrese la descripción de la imagen aquí

Aquí, el editor del evento es el botón, y el suscriptor del evento es la ventana principal. Al aplicar ese diagrama de flujo, hacer una pregunta, ¿se supone que la Ventana principal (suscriptor del evento) está muerta antes que el Botón (editor del evento)? Obviamente No. ¿verdad? Eso ni siquiera tendrá sentido. Entonces, ¿por qué preocuparse por separar el controlador de eventos de clic?

Un ejemplo cuando una separación de controlador de eventos es DEBE.

Proporcionaré un ejemplo donde se supone que el objeto del suscriptor está muerto antes que el objeto del editor. Digamos, su MainWindow publica un evento llamado "SomethingHappened" y usted muestra una ventana secundaria desde la ventana principal haciendo clic en un botón. La ventana secundaria se suscribe a ese evento de la ventana principal.

ingrese la descripción de la imagen aquí

Y, la ventana secundaria se suscribe a un evento de la Ventana principal.

ingrese la descripción de la imagen aquí

A partir de este código, podemos entender claramente que hay un botón en la ventana principal. Al hacer clic en ese botón, se muestra una ventana secundaria. La ventana secundaria escucha un evento desde la ventana principal. Después de hacer algo, el usuario cierra la ventana secundaria.

Ahora, de acuerdo con el diagrama de flujo que proporcioné si hace una pregunta "¿Se supone que la ventana secundaria (suscriptor del evento) está muerta antes que el editor del evento (ventana principal)? La respuesta debería ser SÍ. ¿Correcto? Entonces, separe el controlador de eventos Normalmente lo hago desde el evento descargado de la ventana.

Una regla de oro: si su vista (es decir, WPF, WinForm, UWP, Xamarin Form, etc.) se suscribe a un evento de ViewModel, recuerde siempre separar el controlador de eventos. Porque un ViewModel generalmente dura más que una vista. Por lo tanto, si ViewModel no se destruye, cualquier vista que suscribió un evento de ese ViewModel permanecerá en la memoria, lo que no es bueno.

Prueba del concepto utilizando un generador de perfiles de memoria.

No será muy divertido si no podemos validar el concepto con un generador de perfiles de memoria. He usado el generador de perfiles JetBrain dotMemory en este experimento.

Primero, ejecuté MainWindow, que se muestra así:

ingrese la descripción de la imagen aquí

Entonces, tomé una instantánea de memoria. Luego hice clic en el botón 3 veces . Aparecieron tres ventanas infantiles. He cerrado todas esas ventanas secundarias y he hecho clic en el botón Forzar GC en el generador de perfiles dotMemory para asegurarme de que se llama al recolector de basura. Luego, tomé otra instantánea de memoria y la comparé. ¡Mirad! Nuestro miedo era cierto. El recolector de basura no recogió la ventana infantil incluso después de que se cerraron. No solo eso, sino que el recuento de objetos filtrados para el objeto ChildWindow también se muestra " 3 " (hice clic en el botón 3 veces para mostrar 3 ventanas secundarias).

ingrese la descripción de la imagen aquí

Ok, entonces separé el controlador de eventos como se muestra a continuación.

ingrese la descripción de la imagen aquí

Luego, realicé los mismos pasos y verifiqué el generador de perfiles de memoria. Esta vez, ¡guau! No más pérdida de memoria.

ingrese la descripción de la imagen aquí

Emran Hussain
fuente
3

Un evento es realmente una lista vinculada de controladores de eventos

Cuando haces + = new EventHandler en el evento, realmente no importa si esta función en particular se ha agregado antes como escucha, se agregará una vez por + =.

Cuando se genera el evento, pasa por la lista vinculada, elemento por elemento y llama a todos los métodos (controladores de eventos) agregados a esta lista, es por eso que los controladores de eventos aún se invocan incluso cuando las páginas ya no se ejecutan mientras están vivos (enraizados), y estarán vivos mientras estén conectados. Entonces serán llamados hasta que el controlador de eventos se desenganche con un - = nuevo EventHandler.

Mira aquí

y MSDN AQUÍ

TalentTuner
fuente