Saltar al contenido

¿Cómo abortar una tarea como abortar un hilo (método Thread.Abort)?

Solución:

La guía sobre no usar un aborto de hilo es controvertida. Creo que todavía hay un lugar para ello, pero en circunstancias excepcionales. Sin embargo, siempre debe intentar diseñar a su alrededor y verlo como último recurso.

Ejemplo;

Tiene una aplicación de formulario de Windows simple que se conecta a un servicio web sincrónico de bloqueo. Dentro del cual ejecuta una función en el servicio web dentro de un bucle paralelo.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    Thread.Sleep(120000); // pretend web service call

});

En este ejemplo, digamos que la llamada de bloqueo tarda 2 minutos en completarse. Ahora configuro mi MaxDegreeOfParallelism para que diga ProcessorCount. iListOfItems tiene 1000 elementos para procesar.

El usuario hace clic en el botón de proceso y comienza el ciclo, tenemos ‘hasta’ 20 subprocesos ejecutándose contra 1000 elementos en la colección iListOfItems. Cada iteración se ejecuta en su propio hilo. Cada hilo utilizará un hilo en primer plano cuando sea creado por Parallel.ForEach. Esto significa que, independientemente del cierre de la aplicación principal, el dominio de la aplicación se mantendrá activo hasta que finalicen todos los hilos.

Sin embargo, el usuario debe cerrar la aplicación por alguna razón, digamos que cierra el formulario. Estos 20 subprocesos continuarán ejecutándose hasta que se procesen los 1000 elementos. Esto no es ideal en este escenario, ya que la aplicación no se cerrará como el usuario espera y continuará ejecutándose detrás de escena, como se puede ver al echar un vistazo en el administrador de tareas.

Supongamos que el usuario intenta reconstruir la aplicación nuevamente (VS 2010), informa que el exe está bloqueado, luego tendría que ingresar al administrador de tareas para eliminarlo o simplemente esperar hasta que se procesen los 1000 elementos.

No te culpo por decirlo, ¡pero claro! Debería cancelar estos hilos usando el objeto CancellationTokenSource y llamando a Cancelar … pero hay algunos problemas con esto a partir de .net 4.0. En primer lugar, esto nunca dará como resultado un aborto de subproceso que ofrecería una excepción de anulación seguida de la terminación del subproceso, por lo que el dominio de la aplicación deberá esperar a que los subprocesos terminen normalmente, y esto significa esperar la última llamada de bloqueo, que sería la última iteración en ejecución (hilo) que finalmente llega a llamar po.CancellationToken.ThrowIfCancellationRequested. En el ejemplo, esto significaría que el dominio de la aplicación aún podría permanecer activo hasta 2 minutos, aunque el formulario se haya cerrado y se haya cancelado la llamada.

Tenga en cuenta que Llamar a Cancelar en CancellationTokenSource no genera una excepción en los subprocesos de procesamiento, que de hecho actuarían para interrumpir la llamada de bloqueo de manera similar a un aborto de subprocesos y detener la ejecución. Una excepción se almacena en caché lista para cuando todos los demás subprocesos (iteraciones simultáneas) finalmente terminan y regresan, la excepción se lanza en el subproceso de inicio (donde se declara el bucle).

elegí no para utilizar la opción Cancelar en un objeto CancellationTokenSource. Esto es un desperdicio y posiblemente viola el conocido anti-patrón de controlar el flujo del código por Excepciones.

En su lugar, podría decirse que es “mejor” implementar una propiedad simple segura para subprocesos, es decir, Bool stopExecuting. Luego, dentro del ciclo, verifique el valor de stopExecuting y si el valor se establece en verdadero por la influencia externa, podemos tomar una ruta alternativa para cerrar con gracia. Dado que no debemos llamar a cancelar, esto excluye la verificación de CancellationTokenSource.IsCancellationRequested, que de otro modo sería otra opción.

Algo como la siguiente condición if sería apropiada dentro del ciclo;

if (loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting) {loopState.Stop (); regreso;}

La iteración ahora saldrá de una manera ‘controlada’ y terminará otras iteraciones, pero como dije, esto hace poco para nuestro problema de tener que esperar en las llamadas de larga ejecución y bloqueo que se realizan dentro de cada iteración ( hilo de bucle paralelo), ya que estos deben completarse antes de que cada hilo pueda acceder a la opción de comprobar si debe detenerse.

En resumen, cuando el usuario cierra el formulario, se indicará a los 20 subprocesos que se detengan mediante stopExecuting, pero solo se detendrán cuando hayan terminado de ejecutar su llamada de función de larga duración.

No podemos hacer nada sobre el hecho de que el dominio de la aplicación siempre permanecerá activo y solo se lanzará cuando se hayan completado todos los subprocesos en primer plano. Y esto significa que habrá un retraso asociado con la espera de que se complete cualquier llamada de bloqueo realizada dentro del bucle.

Solo un aborto verdadero del hilo puede interrumpir la llamada de bloqueo, y debe mitigar dejar el sistema en un estado inestable / indefinido lo mejor que pueda en el manejador de excepciones del hilo abortado, lo cual es indudable. Si eso es apropiado es una cuestión que debe decidir el programador, en función de los manejadores de recursos que eligieron mantener y lo fácil que es cerrarlos en el bloque final de un hilo. Puede registrarse con un token para terminar al cancelar como una solución parcial, es decir,

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    using (cts.Token.Register(Thread.CurrentThread.Abort))
    {
        Try
        {
           Thread.Sleep(120000); // pretend web service call          
        }
        Catch(ThreadAbortException ex)
        {
           // log etc.
        }
        Finally
        {
          // clean up here
        }
    }

});

pero esto aún resultará en una excepción en el hilo de declaración.

A fin de cuentas, las llamadas de bloqueo de interrupciones utilizando las construcciones paralelas.loop podrían haber sido un método en las opciones, evitando el uso de partes más oscuras de la biblioteca. Pero el por qué no hay una opción para cancelar y evitar lanzar una excepción en el método de declaración me parece un posible descuido.

Pero, ¿puedo abortar una Tarea (en .Net 4.0) de la misma manera no mediante el mecanismo de cancelación? Quiero eliminar la tarea de inmediato..

Otras personas que respondieron le han dicho que no lo haga. Pero si tu pueden hazlo. Usted puede suministrar Thread.Abort() como el delegado a ser llamado por el mecanismo de cancelación de la Tarea. Así es como puede configurar esto:

class HardAborter
{
  public bool WasAborted { get; private set; }
  private CancellationTokenSource Canceller { get; set; }
  private Task<object> Worker { get; set; }

  public void Start(Func<object> DoFunc)
  {
    WasAborted = false;

    // start a task with a means to do a hard abort (unsafe!)
    Canceller = new CancellationTokenSource();

    Worker = Task.Factory.StartNew(() => 
      {
        try
        {
          // specify this thread's Abort() as the cancel delegate
          using (Canceller.Token.Register(Thread.CurrentThread.Abort))
          {
            return DoFunc();
          }
        }
        catch (ThreadAbortException)
        {
          WasAborted = true;
          return false;
        }
      }, Canceller.Token);
  }

  public void Abort()
  {
    Canceller.Cancel();
  }

}

Descargo de responsabilidad: no hagas esto.

A continuación, se muestra un ejemplo de lo que no se debe hacer:

 var doNotDoThis = new HardAborter();

 // start a thread writing to the console
 doNotDoThis.Start(() =>
    {
       while (true)
       {
          Thread.Sleep(100);
          Console.Write(".");
       }
       return null;
    });


 // wait a second to see some output and show the WasAborted value as false
 Thread.Sleep(1000);
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted);

 // wait another second, abort, and print the time
 Thread.Sleep(1000);
 doNotDoThis.Abort();
 Console.WriteLine("Abort triggered at " + DateTime.Now);

 // wait until the abort finishes and print the time
 while (!doNotDoThis.WasAborted) { Thread.CurrentThread.Join(0); }
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted + " at " + DateTime.Now);

 Console.ReadKey();

salida del código de muestra

  1. No deberías usar Thread.Abort ()
  2. Las tareas pueden cancelarse pero no cancelarse.

El método Thread.Abort () está (severamente) en desuso.

Tanto los subprocesos como las tareas deben cooperar cuando se detengan; de lo contrario, corre el riesgo de dejar el sistema en un estado inestable / indefinido.

Si necesita ejecutar un proceso y eliminarlo desde el exterior, la única opción segura es ejecutarlo en un AppDomain separado.


Esta respuesta es sobre .net 3.5 y versiones anteriores.

El manejo de abortos de subprocesos se ha mejorado desde entonces, entre otras cosas, al cambiar la forma en que finalmente los bloques funcionan.

Pero Thread.Abort sigue siendo una solución sospechosa que siempre debe intentar evitar.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *