¿Se considera aceptable no llamar a Dispose () en un objeto de tarea TPL?

123

Quiero activar una tarea para que se ejecute en un hilo de fondo. No quiero esperar a que se completen las tareas.

En .net 3.5 hubiera hecho esto:

ThreadPool.QueueUserWorkItem(d => { DoSomething(); });

En .net 4, el TPL es la forma sugerida. El patrón común que he visto recomendado es:

Task.Factory.StartNew(() => { DoSomething(); });

Sin embargo, el StartNew()método devuelve un Taskobjeto que implementa IDisposable. Esto parece ser pasado por alto por las personas que recomiendan este patrón. La documentación de MSDN sobre el Task.Dispose()método dice:

"Siempre llame a Eliminar antes de liberar su última referencia a la Tarea".

No puede invocar la disposición en una tarea hasta que se complete, por lo que, en primer lugar, hacer que el subproceso principal espere y elimine la llamada anularía el punto de hacerlo en un subproceso en segundo plano. Tampoco parece haber ningún evento completado / terminado que pueda usarse para la limpieza.

La página de MSDN en la clase Tarea no comenta sobre esto, y el libro "Pro C # 2010 ..." recomienda el mismo patrón y no hace ningún comentario sobre la eliminación de tareas.

Sé que si lo dejo, el finalizador lo atrapará al final, pero ¿volverá esto y me morderá cuando estoy haciendo mucho fuego y olvido tareas como esta y el hilo del finalizador se abruma?

Entonces mis preguntas son:

  • ¿Es aceptable no llamar Dispose()a la Taskclase en este caso? Y si es así, ¿por qué hay riesgos / consecuencias?
  • ¿Hay alguna documentación que discuta esto?
  • ¿O hay una forma adecuada de deshacerse del Taskobjeto que me he perdido?
  • ¿O hay otra forma de disparar y olvidar tareas con el TPL?
Simon P Stevens
fuente
1
Relacionado: La forma correcta de disparar y olvidar (ver respuesta )
Simon P Stevens

Respuestas:

108

Hay una discusión sobre esto en los foros de MSDN .

Stephen Toub, miembro del equipo pfx de Microsoft tiene esto que decir:

Task.Dispose existe debido a que la tarea potencialmente envuelve un identificador de evento utilizado cuando se espera que se complete la tarea, en el caso de que el hilo de espera realmente tenga que bloquearse (en lugar de girar o potencialmente ejecutar la tarea que está esperando). Si todo lo que está haciendo es continuar, ese controlador de eventos nunca se asignará
...
es probable que sea mejor confiar en la finalización para encargarse de las cosas.

Actualización (octubre de 2012)
Stephen Toub ha publicado un blog titulado ¿Necesito deshacerme de las tareas? que proporciona más detalles y explica las mejoras en .Net 4.5.

En resumen: no es necesario deshacerse de los Taskobjetos el 99% del tiempo.

Hay dos razones principales para disponer de un objeto: liberar recursos no administrados de manera oportuna y determinista, y evitar el costo de ejecutar el finalizador del objeto. Ninguno de estos se aplica a la Taskmayoría de las veces:

  1. A partir de .Net 4.5, el único momento en que a Taskasigna el controlador de espera interno (el único recurso no administrado en el Taskobjeto) es cuando utiliza explícitamente el IAsyncResult.AsyncWaitHandlede Task, y
  2. El Taskobjeto en sí no tiene un finalizador; el controlador está envuelto en un objeto con un finalizador, por lo que, a menos que esté asignado, no hay finalizador para ejecutar.
Kirill Muzykov
fuente
3
Gracias interesante. Sin embargo, va en contra de la documentación de MSDN. ¿Hay alguna palabra oficial de MS o del equipo .net de que este es un código aceptable? También está el punto planteado al final de esa discusión que "¿y si la implementación cambia en una versión futura"
Simon P Stevens
En realidad, me acabo de dar cuenta de que el que responde en ese hilo realmente trabaja en microsoft, aparentemente en el equipo de pfx, así que supongo que esta es una especie de respuesta oficial. Pero se sugiere que, en el fondo, no funciona en todos los casos. Si hay una fuga potencial, ¿es mejor que solo regrese a ThreadPool.QueueUserWorkItem que sé que es seguro?
Simon P Stevens
Sí, es muy extraño que haya un Dispose al que no puedas llamar. Si echa un vistazo a las muestras aquí msdn.microsoft.com/en-us/library/dd537610.aspx y aquí msdn.microsoft.com/en-us/library/dd537609.aspx no están eliminando tareas. Sin embargo, los ejemplos de código en MSDN a veces demuestran técnicas muy malas. También el chico respondió que la pregunta funciona para Microsoft.
Kirill Muzykov
2
@Simon: (1) El documento de MSDN que cita es el consejo genérico, los casos específicos tienen consejos más específicos (por ejemplo, no es necesario usar EndInvokeen WinForms cuando se usa BeginInvokepara ejecutar código en el hilo de la interfaz de usuario). (2) Stephen Toub es bastante conocido como un orador habitual sobre el uso efectivo de PFX (por ejemplo, en channel9.msdn.com ), por lo que si alguien puede dar una buena guía, entonces es él. Tenga en cuenta su segundo párrafo: hay veces que dejar las cosas al finalizador es mejor.
Richard
12

Este es el mismo tipo de problema que con la clase Thread. Consume 5 manejadores del sistema operativo pero no implementa IDisposable. Buena decisión de los diseñadores originales, por supuesto, hay pocas formas razonables de llamar al método Dispose (). Tendría que llamar a Join () primero.

La clase Task agrega un identificador a esto, un evento de reinicio manual interno. Cuál es el recurso del sistema operativo más barato que existe. Por supuesto, su método Dispose () solo puede liberar ese identificador de evento, no los 5 identificadores que consume Thread. Sí, no te molestes .

Tenga cuidado de que debería estar interesado en la propiedad IsFaulted de la tarea. Es un tema bastante feo, puede leer más al respecto en este artículo de la Biblioteca de MSDN . Una vez que maneje esto adecuadamente, también debe tener un buen lugar en su código para deshacerse de las tareas.

Hans Passant
fuente
66
Pero una tarea no crea una Threaden la mayoría de los casos, usa ThreadPool.
svick
-1

Me encantaría ver a alguien sopesar la técnica que se muestra en esta publicación: Invocación de delegado asincrónico de tipo seguro para disparar y olvidar en C #

Parece que un método de extensión simple manejará todos los casos triviales de interactuar con las tareas y podrá llamar a disposed en él.

public static void FireAndForget<T>(this Action<T> act,T arg1)
{
    var tsk = Task.Factory.StartNew( ()=> act(arg1),
                                     TaskCreationOptions.LongRunning);
    tsk.ContinueWith(cnt => cnt.Dispose());
}
Chris Marisic
fuente
3
Por supuesto, eso no elimina la Taskinstancia devuelta por ContinueWith, pero ver la cita de Stephen Toub es la respuesta aceptada: no hay nada que eliminar si nada realiza una espera de bloqueo en una tarea.
Richard
1
Como menciona Richard, ContinueWith (...) también devuelve un segundo objeto Task que luego no se elimina.
Simon P Stevens
1
Por lo tanto, el código ContinueWith es realmente peor que el reductor, ya que hará que se cree otra tarea solo para deshacerse de la tarea anterior. Con la forma en que esto se mantiene, básicamente no sería posible introducir una espera de bloqueo en este bloque de código, aparte de si el delegado de acción que le pasó estaba tratando de manipular las Tareas en sí, ¿también correcto?
Chris Marisic
1
Es posible que pueda usar cómo las lambdas capturan variables de una manera un poco complicada para ocuparse de la segunda tarea. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth
@GideonEngelberth que aparentemente tendría que funcionar. Dado que el GC nunca debe deshacerse de Disper, debe seguir siendo válido hasta que el lambda se invoca para deshacerse, suponiendo que la referencia todavía sea válida allí / encogerse de hombros. Tal vez necesita un intento vacío / atraparlo?
Chris Marisic