2009-11-21 9 views
14

Hubo mucha discusión sobre esto y todos tienden a estar de acuerdo en que siempre debe llamar a Delegate.EndInvoke para evitar una pérdida de memoria (¡incluso Jon Skeet lo dijo!).No llamar a Delegate.EndInvoke puede causar pérdida de memoria ... ¿un mito?

Siempre seguí esta guía sin cuestionamientos, pero recientemente implementé mi propia clase AsyncResult y vi que el único recurso que podría tener fugas es AsyncWaitHandle.

(De hecho, no se filtra realmente porque el recurso nativo utilizado por el WaitHandle está encapsulado en un SafeHandle que tiene un Finalizer, sin embargo, agregará presión a la cola de finalización del recolector de elementos no utilizados). implementación de AsyncResult sólo inicializar el AsyncWaitHandle en la demanda ...)

la mejor manera de saber si hay una fuga es sólo para probarlo:

Action a = delegate { }; 
while (true) 
    a.BeginInvoke(null, null); 

me corrieron esto por un tiempo y la memoria mantenerse entre 9-20 MB.

Vamos a comparar con Delegate.EndInvoke cuando se llama:

Action a = delegate { }; 
while (true) 
    a.BeginInvoke(ar => a.EndInvoke(ar), null); 

Con esta prueba, el juego de memoria entre 9-30 MG, raro eh? (Probablemente porque tarda un poco más en ejecutarse cuando hay un AsyncCallback, por lo que habrá más delegados en cola en el ThreadPool)

¿Qué piensas ... "Myth busted"?

P.S. ThreadPool.QueueUserWorkItem es cien veces más eficiente que Delegate.BeginInvoke, es mejor usarlo para disparar & olvidar llamadas.

+2

¡Buena pregunta! +1. Por cierto, ves demasiados MythBusters;) – RCIX

+0

Acepto, ThreadPool.QueueUserWorkItem es un método brillante y poco utilizado. – Anton

Respuesta

6

Realicé una pequeña prueba para llamar a un delegado de Acción y lanzar una excepción dentro de él.Luego, me aseguro de no inundar el grupo de subprocesos manteniendo solo una cantidad específica de subprocesos ejecutándose a la vez y llenando el grupo de subprocesos continuamente cuando una llamada de eliminación ha finalizado. Aquí está el código:

static void Main(string[] args) 
{ 

    const int width = 2; 
    int currentWidth = 0; 
    int totalCalls = 0; 
    Action acc =() => 
    { 
     try 
     { 
      Interlocked.Increment(ref totalCalls); 
      Interlocked.Increment(ref currentWidth); 
      throw new InvalidCastException("test Exception"); 
     } 
     finally 
     { 
      Interlocked.Decrement(ref currentWidth); 
     } 
    }; 

    while (true) 
    { 
     if (currentWidth < width) 
     { 
      for(int i=0;i<width;i++) 
       acc.BeginInvoke(null, null); 
     } 

     if (totalCalls % 1000 == 0) 
      Console.WriteLine("called {0:N}", totalCalls); 

    } 
} 

Después de dejar correr el agua durante unos 20 minutos y más de 30 millones BeginInvoke llama después el consumo de memoria de bytes privados se mantuvo constante (23 MB), así como el número de identificador. Parece que no hay fugas para estar presente. He leído el libro de Jeffry Richters C# a través de CLR donde dice que hay una pérdida de memoria. Al menos esto parece no ser cierto con .NET 3.5 SP1.

entorno de prueba: Windows 7 x86 .NET 3.5 SP1 Intel 6600 de doble núcleo de 2,4 GHz

Suyo, Alois Kraus

2

En algunas situaciones, BeginInvoke no necesita EndInvoke (especialmente en los mensajes de la ventana WinForms). Sin embargo, definitivamente hay situaciones en las que esto es importante, como BeginRead y EndRead para la comunicación asíncrona. Si quieres hacer un BeginWrite de "disparar y olvidar", probablemente termines en serios problemas de memoria después de un tiempo.

Por lo tanto, su única prueba no puede ser concluyente. Debe tratar con muchos tipos diferentes de delegados de eventos asincrónicos para tratar su pregunta correctamente.

+0

Probablemente tenga razón en que algunas operaciones asincrónicas realmente requieren una llamada a EndInvoke, estaba hablando de Delegate.BeginInvoke específicamente. –

1

Considere el siguiente ejemplo, que se ejecutó en mi máquina durante unos minutos y alcanzó un conjunto de trabajo de 3,5 GB antes de que decidiera eliminarlo.

Action a = delegate { throw new InvalidOperationException(); }; 
while (true) 
    a.BeginInvoke(null, null); 

NOTA: Asegúrese de ejecutar sin un depurador asociado o con "rotura en excepción lanzada" y "romper en una excepción no controlada-usuario" discapacitado.

EDIT: Como señala Jeff, el problema de la memoria aquí no es una fuga, sino simplemente un caso de abrumador del sistema al poner en cola el trabajo más rápido de lo que se puede procesar. De hecho, el mismo comportamiento se puede observar al reemplazar el lanzamiento con cualquier operación larga adecuada. Y el uso de la memoria está limitado si dejamos suficiente tiempo entre las llamadas a BeginInvoke.

Técnicamente, eso deja la pregunta original sin respuesta. Sin embargo, independientemente de si puede o no provocar una fuga, no llamar a Delegate.EndInvoke es una mala idea, ya que puede hacer que se ignoren las excepciones.

+1

Interesante, ¿pero has probado esto? Acción a = delegar {throw new InvalidOperationException(); }; while (true) a.BeginInvoke (ar => { tratar { a.EndInvoke (AR); } catch {} }, null); El consumo de memoria tiene el mismo aspecto cuando se llama a EndInvoke, la memoria probablemente sea demasiado alta solo porque pone en cola más elementos de trabajo en la cola de lo que puede ejecutarlos cuando lanza una excepción. –

+0

Tienes razón. He corregido mi respuesta. –

11

Sea o no se escapa actualmente memoria no es algo que debe depender . El equipo del marco podría, en el futuro, cambiar las cosas de una manera que podría causar una fuga, y debido a que la política oficial es "debe llamar a EndInvoke", entonces es "por diseño".

¿Realmente quiere correr el riesgo de que su aplicación empiece a perder repentinamente la memoria en algún momento en el futuro porque eligió confiar en el comportamiento observado sobre los requisitos documentados?

+2

+1. Además, no hay necesidad de especular sobre los peligros en una futura revisión del marco. Como señalé en mi respuesta, dejando de lado la posibilidad de pérdida de memoria, no llamar a 'Delegate.EndInvoke' hará que se ignoren las excepciones y no le dará ninguna forma de saber si la acción se completó correctamente o no. –

Cuestiones relacionadas