¿Cuándo deshacerse de CancellationTokenSource?

La clase CancellationTokenSource es desechable. Un vistazo rápido en Reflector demuestra el uso de KernelEvent , un KernelEvent (muy probable) no administrado. Dado que CancellationTokenSource no tiene un finalizador, si no lo eliminamos, el GC no lo hará.

Por otro lado, si observa los ejemplos enumerados en la cancelación de artículos de MSDN en hilos gestionados , solo un fragmento de código descarta el token.

¿Cuál es la forma correcta de eliminarlo en el código?

  1. No puede ajustar el código comenzando su tarea paralela con el using si no lo espera. Y tiene sentido tener cancelación solo si no esperas.
  2. Por supuesto, puede agregar ContinueWith en la tarea con una llamada a Dispose , pero ¿es ese el camino a seguir?
  3. ¿Qué sucede con las consultas cancelables de PLINQ, que no se sincronizan sino que simplemente hacen algo al final? Digamos .ForAll(x => Console.Write(x)) ?
  4. ¿Es reutilizable? ¿Se puede usar el mismo token para varias llamadas y luego disponerlo junto con el componente de host, digamos el control de UI?

Como no tiene algo así como un método de Reset para limpiar el campo IsCancelRequested y Token , supongo que no es reutilizable, por lo que cada vez que inicie una tarea (o una consulta PLINQ) debe crear una nueva. ¿Es verdad? En caso afirmativo, mi pregunta es ¿cuál es la estrategia correcta y recomendada para tratar el tema Dispose en esas muchas instancias de CancellationTokenSource ?

Hablando sobre si es realmente necesario llamar a Dispose en CancellationTokenSource … tuve una pérdida de memoria en mi proyecto y resultó que CancellationTokenSource era el problema.

Mi proyecto tiene un servicio, que constantemente lee la base de datos y dispara diferentes tareas, y estaba pasando tokens de cancelación vinculados a mis trabajadores, por lo que incluso después de que habían terminado de procesar los datos, los tokens de cancelación no se eliminaban, lo que causaba una pérdida de memoria.

La cancelación de MSDN en Managed Threads lo dice claramente:

Tenga en cuenta que debe llamar a Dispose en la fuente del token vinculado cuando haya terminado con él. Para obtener un ejemplo más completo, consulte Cómo: Escuchar múltiples solicitudes de cancelación .

Usé ContinueWith en mi implementación.

Eché un vistazo en ILSpy para el CancellationTokenSource pero solo puedo encontrar m_KernelEvent, que en realidad es un ManualResetEvent , que es una clase contenedora para un objeto WaitHandle. Esto debe ser manejado apropiadamente por el GC.

No pensé que ninguna de las respuestas actuales fuera satisfactoria. Después de investigar encontré esta respuesta de Stephen Toub ( referencia ):

Depende. En .NET 4, CTS.Dispose sirvió para dos propósitos principales. Si se ha accedido al WaitHandle de CancellationToken (asignándolo de forma perezosa), Dispose eliminará ese handle. Además, si el CTS se creó mediante el método CreateLinkedTokenSource, Dispose desvinculará el CTS de los tokens a los que se vinculó. En .NET 4.5, Dispose tiene un propósito adicional, que es si el CTS usa un temporizador debajo de las cubiertas (por ejemplo, cancel-after fue llamado), el temporizador será Disposed.

Es muy raro que CancellationToken.WaitHandle se use, por lo que limpiarlo después de que normalmente no es una buena razón para usar Dispose. Sin embargo, si está creando su CTS con CreateLinkedTokenSource, o si está utilizando la funcionalidad del temporizador del CTS, puede ser más impactante usar Dispose.

La parte audaz, creo que es la parte importante. Él usa “más impactante”, lo que lo deja un poco vago. Lo interpreto como que significa que llamar a Dispose en esas situaciones debe hacerse, de lo contrario, no es necesario usar Dispose .

Siempre debe desechar CancellationTokenSource .

Cómo deshacerse depende exactamente del escenario. Usted propone varios escenarios diferentes.

  1. using solo funciona cuando usa CancellationTokenSource en algún trabajo paralelo que está esperando. Si ese es tu senario, genial, es el método más fácil.

  2. Cuando utilice tareas, use una tarea ContinueWith como indicó para deshacerse de CancellationTokenSource .

  3. Para plinq puedes utilizarlo ya que lo estás ejecutando en paralelo pero esperando a que terminen todos los trabajadores en paralelo.

  4. Para la IU, puede crear un nuevo CancellationTokenSource para cada operación cancelable que no esté vinculada a un único activador de cancelación. Mantenga una List y agregue cada fuente a la lista, eliminándolos cuando su componente sea eliminado.

  5. Para los subprocesos, cree un nuevo subproceso que una todos los subprocesos de trabajo y cierre el origen único cuando finalicen todos los subprocesos de trabajo. Ver CancellationTokenSource, ¿Cuándo desechar?

Siempre hay una manera. IDisposable instancias IDisposable siempre deben eliminarse. Las muestras a menudo no lo hacen porque son muestras rápidas para mostrar el uso del núcleo o porque agregar todos los aspectos de la clase demostrada sería demasiado complejo para una muestra. La muestra es solo una muestra, no necesariamente (o incluso por lo general) código de calidad de producción. No todas las muestras son aceptables para copiarse en el código de producción como está.

Esta respuesta todavía está apareciendo en las búsquedas de Google, y creo que la respuesta votada no brinda la historia completa. Después de revisar el código fuente de CancellationTokenSource (CTS) y CancellationToken (CT) creo que para la mayoría de los casos de uso, la siguiente secuencia de código está bien:

 if (cancelTokenSource != null) { cancelTokenSource.Cancel(); cancelTokenSource.Dispose(); cancelTokenSource = null; } 

El campo interno m_kernelHandle mencionado anteriormente es el objeto de sincronización que respalda la propiedad WaitHandle en las clases CTS y CT. Solo se crea una instancia si accede a esa propiedad. Por lo tanto, a menos que esté usando WaitHandle para alguna sincronización de hilos de la vieja escuela en su llamada de Task disponer no tendrá ningún efecto.

Por supuesto, si lo está utilizando, debe hacer lo que sugieren las otras respuestas anteriores y retrasar la invocación de Dispose hasta que se WaitHandle todas WaitHandle operaciones WaitHandle que utilizan el handle, porque, como se describe en la documentación API de Windows para WaitHandle , los resultados no están definidos .

Cree una nueva aplicación de Windows Forms desde la plantilla del proyecto. Coloque un botón en el formulario y haga doble clic en él. Haz que se vea así:

  private void button1_Click(object sender, EventArgs e) { var t = new System.Threading.Thread(() => { }); t.Start(); } 

Presione Ctrl + F5 para iniciarlo. Start + Run, TaskMgr.exe, pestaña Procesos. Ver + Seleccionar columnas y marcar “Manejar”. Observe el valor de esta columna para el proceso de WindowsFormsApplication1.exe mientras hace clic repetidamente en el botón.

La clase Thread no tiene un método Dispose ().

Vamos a trabajar desde la suposición de que tenía uno. ¿Cuándo lo llamarías?


Lea más sobre la sabiduría de tratar de deshacerse de los objetos difíciles de eliminar en esta publicación de blog de Stephen Toub.

Ha pasado mucho tiempo desde que pregunté esto y obtuve muchas respuestas útiles, pero encontré un tema interesante relacionado con esto y pensé que lo publicaría aquí como otra respuesta de tipo:

Debería llamar a CancellationTokenSource.Dispose() solo cuando esté seguro de que nadie intentará obtener la propiedad Token del CTS. De lo contrario, no deberías llamarlo, porque es una carrera. Por ejemplo, mira aquí:

https://github.com/aspnet/AspNetKatana/issues/108

En la solución para este problema, código que anteriormente hacía cts.Cancel(); cts.Dispose(); cts.Cancel(); cts.Dispose(); fue editado para hacer cts.Cancel(); porque cualquier persona tan desafortunada como tratar de obtener el token de cancelación para observar su estado de cancelación después de haber llamado a Dispose, desafortunadamente también necesitará manejar ObjectDisposedException , además de la OperationCanceledException que estaban planeando.

Tratcher hizo otra observación clave relacionada con esta solución: “La eliminación solo es necesaria para los tokens que no se cancelarán, ya que la cancelación hace la misma limpieza”. es decir, simplemente haciendo Cancel() lugar de eliminar es realmente lo suficientemente bueno!