Pregunta sobre cómo terminar un hilo limpiamente en .NET

Resuelto GONeale asked hace 14 años • 8 respuestas

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 todos mis abortos para reemplazarlos de una manera más limpia; y después de comparar las estrategias de usuario de la gente aquí en stackoverflow y luego de leer " Cómo: crear y terminar subprocesos (Guía de programación de C#) " de MSDN, ambos establecen un enfoque muy similar, que es utilizar una volatile boolestrategia de verificación de enfoque, Lo cual es bueno, pero todavía tengo algunas preguntas....

Inmediatamente, lo que me llama la atención aquí es ¿qué pasa si no tienes un proceso de trabajo simple que simplemente ejecuta 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, reviso cada archivo, así que eso es algo, y seguro que podría agregar mi while (!_shouldStop)en la parte superior, que cubre cada iteración del bucle, pero tengo muchos más procesos de negocios. que ocurren antes de llegar a la siguiente iteración del bucle, quiero que este procedimiento de cancelación sea ágil; ¿No me digas que necesito esparcir estos bucles while cada 4 o 5 líneas durante toda mi función de trabajador?

Realmente espero que haya una manera mejor. ¿Podría alguien aconsejarme 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 me sienta cómodo. ¿Qué pasa si se trata de una operación en segundo plano lineal pero oportuna?

GONeale avatar Sep 03 '10 07:09 GONeale
Aceptado

Desafortunadamente, puede que no haya una mejor opción. Realmente depende de su escenario específico. La idea es detener el hilo con gracia en puntos seguros. Ése es el quid de la razón por la que Thread.Abortno es bueno; porque no se garantiza que ocurra en puntos seguros. Al rociar el código con un mecanismo de parada, efectivamente estás definiendo manualmente los puntos seguros. Esto se llama cancelación cooperativa. Básicamente, existen cuatro mecanismos generales para hacer esto. Podrás elegir el que mejor se adapte a tu situación.

Sondear una bandera de parada

Ya has mencionado este método. Éste es bastante común. Realice comprobaciones periódicas de la bandera en puntos seguros de su algoritmo y salte cuando se le indique. El método estándar es marcar la variable volatile. Si eso no es posible o no es conveniente, puede utilizar un archivo lock. Recuerde, no puede marcar una variable local como volatiletal 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 necesaria. No hay mucho más que decir sobre este método.

Utilice los nuevos mecanismos de cancelación en la TPL

Esto es similar a sondear una bandera de detención excepto que utiliza las nuevas estructuras de datos de cancelación en el TPL. Todavía se basa en patrones de cancelación cooperativa. Necesita obtener CancellationTokenun control periódico IsCancellationRequested. Para solicitar la cancelación, deberá llamar Cancela CancellationTokenSourcequien originalmente proporcionó el token. Hay muchas cosas que puedes hacer con los nuevos mecanismos de cancelación. Puedes leer más sobre aquí .

Utilice manijas de espera

Este método puede resultar útil si su subproceso de trabajo requiere esperar en un intervalo específico o una señal durante su funcionamiento normal. Puedes Set, ManualResetEventpor ejemplo, hacerle saber al hilo que es hora de detenerse. Puede probar el evento utilizando la WaitOnefunción que devuelve un boolindicador que indica si el evento fue señalado. Toma WaitOneun parámetro que especifica cuánto tiempo esperar para que regrese la llamada si el evento no se señaló en ese período de tiempo. Puede utilizar esta técnica en lugar de Thread.Sleepy obtener la indicación de parada al mismo tiempo. También es útil si hay otras WaitHandleinstancias en las que el hilo tenga que esperar. Puede llamar WaitHandle.WaitAnypara esperar cualquier evento (incluido el evento de detención), todo en una sola llamada. Usar un evento puede ser mejor que llamar, Thread.Interruptya que tiene más control sobre el flujo del programa ( Thread.Interruptgenera una excepción, por lo que tendría que colocar los try-catchbloques estratégicamente para realizar cualquier limpieza necesaria).

Escenarios especializados

Existen varios escenarios puntuales que cuentan con mecanismos de parada muy especializados. Definitivamente está fuera del alcance de esta respuesta enumerarlos todos (sin importar que sería casi imposible). Un buen ejemplo de lo que quiero decir aquí es la Socketclase. Si el hilo está bloqueado en una llamada Sendo Receivela llamada Closeinterrumpirá el socket en cualquier llamada de bloqueo para desbloquearlo efectivamente. Estoy seguro de que hay varias otras áreas en BCL donde se pueden usar técnicas similares para desbloquear un hilo.

Interrumpir el hilo medianteThread.Interrupt

La ventaja aquí es que es simple y no tienes que concentrarte en rociar tu código con nada. La desventaja es que tienes poco control sobre dónde están los puntos seguros en tu algoritmo. La razón es que Thread.Interruptfunciona inyectando una excepción dentro de una de las llamadas de bloqueo BCL predefinidas. Estos incluyen Thread.Sleep, WaitHandle.WaitOne, Thread.Join, etc. Por lo tanto, debe tener cuidado al colocarlos. Sin embargo, la mayoría de las veces el algoritmo dicta adónde van y eso suele estar bien de todos modos, especialmente si su algoritmo pasa la mayor parte del tiempo en una de estas llamadas de bloqueo. Si su algoritmo no utiliza una de las llamadas de bloqueo en BCL, entonces este método no funcionará para usted. La teoría aquí es que solo se genera a partir de una llamada en espera de .NET, por lo que probablementeThreadInterruptException se encuentre en un punto seguro. Como mínimo, usted sabe que el hilo no puede estar en código no administrado o salirse de una sección crítica dejando un candado colgando en un estado adquirido. A pesar de que esto es menos invasivo, desaconsejo su uso porque no es obvio qué llamadas responden y muchos desarrolladores no estarán familiarizados con sus matices.Thread.Abort

Brian Gideon avatar Sep 03 '2010 02:09 Brian Gideon

Bueno, desafortunadamente en multithreading a menudo tienes que comprometer la "agilidad" por la limpieza... puedes salir de un hilo inmediatamente si lo haces Interrupt, pero no estará muy limpio. Entonces no, no es necesario que coloques los _shouldStopcontroles cada 4 o 5 líneas, pero si interrumpes el hilo, entonces debes manejar la excepción y salir del bucle de manera limpia.

Actualizar

Incluso si no es un subproceso en bucle (es decir, tal vez sea un subproceso que realiza alguna operación asincrónica de larga duración o algún tipo de bloque para operación de entrada), puede hacerlo Interrupt, pero aun así debe capturar el subproceso ThreadInterruptedExceptiony salir del subproceso limpiamente. Creo que los ejemplos que has estado leyendo son muy apropiados.

Actualización 2.0

Sí, tengo un ejemplo... Solo te mostraré un ejemplo basado en el enlace al que hiciste 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);
    }
}
Kiril avatar Sep 03 '2010 00:09 Kiril

Si la cancelación es un requisito de lo que estás construyendo, entonces debes tratarlo con tanto respeto como el resto de tu código; puede ser algo para lo que tengas 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 núcleo

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

Si está esperando el kernel, entonces probablemente haya un identificador (o fd, o puerto mach, ...) involucrado en la espera. Por lo general, si destruye el identificador relevante, el kernel regresará inmediatamente con algún código de error. Si estás en .net/java/etc. probablemente terminarás con una excepción. En C, cualquier código que ya tenga implementado para manejar fallas de llamadas al sistema propagará el error a una parte significativa de su aplicación. De cualquier manera, sale del lugar de bajo nivel de manera bastante limpia y oportuna sin necesidad de esparcir código nuevo por todas partes.

Una táctica que uso a menudo con este tipo de código es realizar un seguimiento de una lista de identificadores que deben cerrarse y luego hacer que mi función de cancelación establezca un indicador de "cancelación" y luego cerrarlos. Cuando la función falla, puede verificar el indicador e informar el error debido a la cancelación en lugar de a la excepción/errno específico.

Parece estar insinuando que una granularidad aceptable para la cancelación está al nivel de una llamada de servicio. Probablemente esto no sea una buena idea: es mucho mejor cancelar el trabajo en segundo plano de forma sincrónica y unirse al antiguo hilo en segundo plano desde el hilo en 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/hilos ocultos causadas por procesos en segundo plano pendientes al hacer posible que se oculten los efectos de un hilo de fondo pendiente.

Hay dos razones para tener miedo de este enfoque:

  1. No cree que pueda cancelar su propio código de manera oportuna. Si la cancelación es un requisito de su aplicación, la decisión que realmente debe tomar es una decisión de recursos/negocio: piratear o solucionar su problema limpiamente.

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

blucz avatar Sep 03 '2010 00:09 blucz