Pregunta sobre terminar un hilo limpiamente en .NET

Entiendo que Thread.Abort () es malo por la multitud de artículos que he leído sobre el tema, por lo que actualmente estoy en el proceso de eliminar mi aborto para poder reemplazarlo de una manera más limpia; y después de comparar las estrategias de usuario de personas aquí en stackoverflow y luego de leer ” Cómo: Crear y terminar subprocesos (Guía de progtwigción C #) “ de MSDN, que establecen un enfoque muy similar, que es usar una aproximación de volatile bool estrategia, lo cual es bueno, pero aún tengo algunas preguntas …

Inmediatamente, lo que más me llama la atención es si no tienes un proceso de trabajo simple que simplemente esté ejecutando un bucle de código de procesamiento. Por ejemplo, para mí, mi proceso es un proceso de carga de archivos en segundo plano, de hecho recorro cada archivo, así que eso es algo, y seguro que podría agregar mi while (!_shouldStop) en la parte superior que me cubre cada iteración de bucle, pero tiene muchos más procesos de negocios que ocurren antes de que llegue a la siguiente iteración de bucle, quiero que este procedimiento de cancelación sea rápido; ¡No me diga que necesito rociar estos bucles cada 4-5 líneas hacia abajo a lo largo de toda mi función de trabajador!

Realmente espero que haya una mejor manera, ¿podría alguien avisarme si este es, de hecho, el enfoque correcto [y único?] Para hacer esto, o las estrategias que han utilizado en el pasado para lograr lo que busco.

Gracias pandilla.

Lectura adicional: todas estas respuestas SO suponen que el hilo de trabajo se repetirá. Eso no se sienta cómodamente conmigo. ¿Qué pasa si se trata de una operación de fondo lineal, pero oportuna?

Lamentablemente, puede que no haya una mejor opción. Realmente depende de tu escenario específico. La idea es detener el hilo con gracia en los puntos seguros. Ese es el quid de la razón por la cual Thread.Abort no es bueno; porque no está garantizado que ocurra en puntos seguros. Al rociar el código con un mecanismo de parada, efectivamente define manualmente los puntos de seguridad. Esto se llama cancelación cooperativa. Básicamente hay 4 mecanismos amplios para hacer esto. Puedes elegir el que mejor se adapte a tu situación.

Encuesta una bandera de parada

Usted ya ha mencionado este método. Esta es una muy común. Realice verificaciones periódicas de la bandera en puntos seguros en su algoritmo y rescate cuando se señalice. El enfoque estándar es marcar la variable volatile . Si eso no es posible o no es conveniente, puede usar un lock . Recuerde, no puede marcar una variable local como volatile por lo que si una expresión lambda la captura a través de un cierre, por ejemplo, entonces tendría que recurrir a un método diferente para crear la barrera de memoria que se requiere. No hay mucho más que decir sobre este método.

Utilice los nuevos mecanismos de cancelación en el TPL

Esto es similar al sondeo de una bandera de parada, excepto que utiliza las nuevas estructuras de datos de cancelación en el TPL. Todavía se basa en patrones de cancelación cooperativos. IsCancellationRequested obtener un CancellationToken y verificar periódicamente IsCancellationRequested . Para solicitar la cancelación, debe llamar a Cancel en CancellationTokenSource que originalmente proporcionó el token. Hay mucho que puede hacer con los nuevos mecanismos de cancelación. Puedes leer más sobre esto .

Use las manijas de espera

Este método puede ser útil si su hilo de trabajo requiere esperar en un intervalo específico o una señal durante su operación normal. Puede Set un ManualResetEvent , por ejemplo, para que el hilo sepa que es hora de detenerse. Puede probar el evento usando la función WaitOne que devuelve un bool indica si el evento fue señalado. WaitOne toma un parámetro que especifica cuánto tiempo esperar para que la llamada regrese si el evento no se señalizó en ese período de tiempo. Puede utilizar esta técnica en lugar de Thread.Sleep y obtener la indicación de parada al mismo tiempo. También es útil si hay otras instancias WaitHandle que el hilo puede tener que esperar. Puede llamar a WaitHandle.WaitAny para esperar en cualquier evento (incluido el evento de detención) todo en una llamada. Usar un evento puede ser mejor que llamar a Thread.Interrupt ya que tienes más control sobre el flujo del progtwig ( Thread.Interrupt arroja una excepción por lo que tendrías que colocar estratégicamente los bloques try-catch para realizar cualquier limpieza necesaria).

Escenarios especializados

Hay varios escenarios únicos que tienen mecanismos de detención muy especializados. Definitivamente está fuera del scope de esta respuesta enumerarlos a todos (no importa que sería casi imposible). Un buen ejemplo de lo que quiero decir aquí es la clase Socket . Si el hilo está bloqueado en una llamada a Send o Receive , al llamar Close se interrumpirá el socket en cualquier llamada de locking que esté efectivamente desbloqueándolo. Estoy seguro de que hay muchas otras áreas en el BCL donde se pueden usar técnicas similares para desbloquear un hilo.

Interrumpa el hilo a través de Thread.Interrupt

La ventaja aquí es que es simple y no tienes que concentrarte en rociar tu código con nada realmente. La desventaja es que tiene poco control sobre dónde están los puntos seguros en su algoritmo. El motivo es porque Thread.Interrupt funciona inyectando una excepción dentro de una de las llamadas de locking de BCL enlatadas. Estos incluyen Thread.Sleep , WaitHandle.WaitOne , Thread.Join , etc. Así que debes ser prudente sobre dónde los Thread.Join . Sin embargo, la mayor parte del tiempo el algoritmo dicta dónde van y, por lo general, está bien, especialmente si el algoritmo pasa la mayor parte del tiempo en una de estas llamadas de locking. Si su algoritmo no usa una de las llamadas de locking en el BCL, entonces este método no funcionará para usted. La teoría aquí es que ThreadInterruptException solo se genera desde la llamada en espera de .NET, por lo que es probable que se encuentre en un punto seguro. Por lo menos, usted sabe que el hilo no puede estar en código no administrado o rescatar de una sección crítica dejando un locking colgante en un estado adquirido. A pesar de ser menos invasivo que Thread.Abort , aún desaconsejo su uso porque no es obvio qué llamadas responden y muchos desarrolladores no estarán familiarizados con sus matices.

Bueno, desafortunadamente en el multihilo a menudo tienes que comprometer la “ligereza” para la limpieza … puedes salir de un hilo inmediatamente si lo Interrupt , pero no será muy limpio. Entonces, no, no tiene que rociar las comprobaciones de _shouldStop cada 4-5 líneas, pero si interrumpe el hilo, debe manejar la excepción y salir del ciclo de una manera limpia.

Actualizar

Incluso si no es un hilo de bucle (es decir, quizás es un hilo que realiza alguna operación asíncrona de larga ejecución o algún tipo de bloque para la operación de entrada), puede Interrupt , pero aún debe atrapar la ThreadInterruptedException y salir del hilo limpiamente. Creo que los ejemplos que has estado leyendo son muy apropiados.

Actualización 2.0

Sí, tengo un ejemplo … Le mostraré un ejemplo basado en el enlace al que hizo referencia:

 public class InterruptExample { private Thread t; private volatile boolean alive; public InterruptExample() { alive = false; t = new Thread(()=> { try { while (alive) { /* Do work. */ } } catch (ThreadInterruptedException exception) { /* Clean up. */ } }); t.IsBackground = true; } public void Start() { alive = true; t.Start(); } public void Kill(int timeout = 0) { // somebody tells you to stop the thread t.Interrupt(); // Optionally you can block the caller // by making them wait until the thread exits. // If they leave the default timeout, // then they will not wait at all t.Join(timeout); } } 

Si la cancelación es un requisito de lo que está creando, entonces debe tratarse con tanto respeto como el rest de su código; puede ser algo para lo que tenga que diseñar.

Supongamos que su hilo está haciendo una de dos cosas en todo momento.

  1. Algo vinculado a la CPU
  2. Esperando el kernel

Si está vinculado a la CPU en el hilo en cuestión, probablemente tenga un buen lugar para insertar el cheque de rescate. Si llama al código de otra persona para realizar alguna tarea de larga duración vinculada a la CPU, entonces es posible que tenga que arreglar el código externo, moverlo fuera del proceso (abortar los hilos es malo, pero abortar procesos está bien definido y es seguro ), etc.

Si está esperando el kernel, probablemente haya un manejador (o fd, o mach port, …) involucrado en la espera. Por lo general, si destruyes el identificador relevante, el kernel volverá con algún código de falla inmediatamente. Si estás en .net / java / etc. es probable que termine con una excepción. En C, cualquier código que ya tenga implementado para manejar las fallas de las llamadas al sistema propagará el error a una parte significativa de su aplicación. De cualquier forma, sales del lugar de bajo nivel bastante limpiamente y de manera oportuna sin necesidad de tener un nuevo código salpicado en todas partes.

Una táctica que a menudo uso con este tipo de código es hacer un seguimiento de una lista de identificadores que deben cerrarse y luego hacer que mi función de aborto establezca una bandera “cancelada” y luego cerrarlos. Cuando la función falla, puede verificar el indicador y notificar el error debido a la cancelación en lugar de a causa de la excepción específica / errno.

Pareces estar dando a entender que una granularidad aceptable para la cancelación se encuentra en el nivel de una llamada de servicio. Probablemente esto no sea una buena idea: es mucho mejor cancelar el trabajo de fondo sincrónicamente y unir el antiguo hilo de fondo del hilo de primer plano. Es mucho más limpio porque:

  1. Evita una clase de condiciones de carrera cuando los viejos hilos de bgwork vuelven a la vida después de retrasos inesperados.

  2. Evita posibles pérdidas de memoria / subprocesos ocultos causados ​​por procesos de fondo colgantes al posibilitar que se oculten los efectos de un hilo de fondo colgante.

Hay dos razones para tener miedo de este enfoque:

  1. No cree que pueda abortar su propio código de manera oportuna. Si la cancelación es un requisito de su aplicación, la decisión que realmente necesita tomar es una decisión de recursos / negocio: haga un truco o solucione su problema de forma limpia.

  2. No confías en algún código al que llamas porque está fuera de tu control. Si realmente no confías en él, considera moverlo fuera del proceso. Se obtiene un aislamiento mucho mejor de muchos tipos de riesgos, incluido este, de esa manera.

Quizás una parte del problema es que tienes un método / ciclo tan largo. Ya sea que tenga problemas para enhebrar o no, debe dividirlo en pasos de procesamiento más pequeños. Supongamos que esos pasos son Alpha (), Bravo (), Charlie () y Delta ().

Entonces podrías hacer algo como esto:

  public void MyBigBackgroundTask() { Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta }; int workStepSize = 0; while (!_shouldStop) { tasks[workStepSize++](); workStepSize %= tasks.Length; }; } 

Entonces sí, se repite sin cesar, pero verifica si es el momento de detenerse entre cada paso del negocio.

No es necesario rociar mientras bucles en todas partes. El bucle while externo solo comprueba si se le ha dicho que se detenga y, de ser así, no realiza otra iteración …

Si tienes un hilo recto “haz algo y cierra” (sin bucles), entonces simplemente verificas el _shouldStop booleano antes o después de cada punto principal dentro del hilo. De esa forma sabrá si debe continuar o rescatar.

por ejemplo:

 public void DoWork() { RunSomeBigMethod(); if (_shouldStop){ return; } RunSomeOtherBigMethod(); if (_shouldStop){ return; } //.... } 

La mejor respuesta depende en gran medida de lo que estás haciendo en el hilo.

  • Como dijiste, la mayoría de las respuestas giran en torno a un booleano compartido en cada línea de pareja. Aunque no le guste, este es a menudo el esquema más simple. Si quieres hacerte la vida más fácil, puedes escribir un método como ThrowIfCancelled (), que arroja algún tipo de excepción si ya terminaste. Los puristas dirán que esto es (jadeo) usando excepciones para el flujo de control, pero de nuevo, la cecelación es excepcional.

  • Si está haciendo operaciones IO (como cosas de red), puede considerar hacer todo usando operaciones asincrónicas.

  • Si está haciendo una secuencia de pasos, puede usar el truco IEnumerable para crear una máquina de estados. Ejemplo:

<

 abstract class StateMachine : IDisposable { public abstract IEnumerable Main(); public virtual void Dispose() { /// ... override with free-ing code ... } bool wasCancelled; public bool Cancel() { // ... set wasCancelled using locking scheme of choice ... } public Thread Run() { var thread = new Thread(() => { try { if(wasCancelled) return; foreach(var x in Main()) { if(wasCancelled) return; } } finally { Dispose(); } }); thread.Start() } } class MyStateMachine : StateMachine { public override IEnumerabl Main() { DoSomething(); yield return null; DoSomethingElse(); yield return null; } } // then call new MyStateMachine().Run() to run. 

>

¿Overengineering? Depende de cuántas máquinas de estado use. Si solo tienes 1, sí. Si tienes 100, entonces tal vez no. Demasiado complicado? Bueno, eso depende. Otra ventaja de este enfoque es que le permite (con pequeñas modificaciones) mover su operación a una callback Timer.tick y anular el enhebrado si tiene sentido.

y haz todo lo que dice blucz también.

En lugar de agregar un bucle while al que no pertenece un bucle, agregue algo como if (_shouldStop) CleanupAndExit(); donde sea que tenga sentido hacerlo No es necesario verificar después de cada operación o rociar el código con ellas. En su lugar, piense en cada cheque como una oportunidad para salir del hilo en ese punto y agréguelos estratégicamente con esto en mente.

Todas estas respuestas SO suponen que el hilo de trabajo se repetirá. Eso no se sienta cómodamente conmigo

No hay muchas maneras de hacer que el código tome mucho tiempo. Looping es una construcción de progtwigción bastante esencial. Hacer código lleva mucho tiempo sin bucles toma una gran cantidad de declaraciones. Cientos de miles.

O llamando a otro código que hace el bucle por usted. Sí, es difícil hacer que el código se detenga a demanda. Eso simplemente no funciona.