Para ver el artículo en inglés, active la casilla Inglés. También puede ver el texto en inglés en una ventana emergente si pasa el puntero del mouse por el texto.
Traducción
Inglés

Cancellation in Managed Threads

.NET Framework (current version)
 

A partir de .NET Framework 4, .NET Framework usa un modelo unificado para la cancelación cooperativa de operaciones asincrónicas o sincrónicas de ejecución prolongada. Este modelo se basa en un objeto ligero denominado token de cancelación. El objeto que invoca una o más operaciones cancelables, por ejemplo creando un nuevo subproceso o tarea, pasa el token a cada operación. Las operaciones individuales pueden pasar a su vez copias del token a otras operaciones. En algún momento posterior, el objeto que creó el token puede usarlo para solicitar que las operaciones se detengan. Solo el objeto solicitante puede emitir la solicitud de cancelación y cada agente de escucha es responsable de observar la solicitud y responder a ella de manera puntual.

El patrón general para implementar el modelo de cancelación cooperativa es:

  • Crear una instancia de un objeto CancellationTokenSource, que administra y envía una notificación de cancelación a los tokens de cancelación individuales.

  • Pasar el token devuelto por la propiedad CancellationTokenSource.Token para cada tarea o el subproceso que realiza escuchas de cancelación.

  • Proporcionar un mecanismo para que cada tarea o subproceso responda a la cancelación.

  • Llamar al método CancellationTokenSource.Cancel para proporcionar una notificación de cancelación.

System_CAPS_importantImportante

La clase CancellationTokenSource implementa la interfaz IDisposable. Debe asegurarse de llamar al método CancellationTokenSource.Dispose cuando termine de usar el origen del token de cancelación para liberar los recursos no administrados que contiene.

En la siguiente ilustración se muestra la relación entre un origen de token y todas las copias de su token.

CancellationTokenSource y tokens de cancelación

El nuevo modelo de cancelación facilita la creación de bibliotecas y aplicaciones compatibles con la cancelación y admite las siguientes características:

  • La cancelación es cooperativa y no se impone al agente de escucha. El agente de escucha determina cómo finalizar correctamente en respuesta a una solicitud de cancelación.

  • La solicitud es distinta que la escucha. Un objeto que invoca una operación cancelable puede controlar cuándo se solicita la cancelación (si se solicita).

  • El objeto solicitante emite la solicitud de cancelación para todas las copias del token usando simplemente una llamada de método.

  • Si los une en un token vinculado, un agente de escucha puede escuchar varios tokens simultáneamente

  • El código de usuario puede observar y responder a las solicitudes de cancelación desde código de biblioteca y el código de biblioteca puede observar y responder a las solicitudes de cancelación desde código de usuario.

  • Los agentes de escucha pueden recibir las solicitudes de cancelación mediante sondeo, registro de devolución de llamada o espera en identificadores de espera.

El marco de cancelación se implementa como un conjunto de tipos relacionados. Estos tipos se enumeran en la tabla siguiente.

Nombre de tipo

Descripción

CancellationTokenSource

Objeto que se crea un token de cancelación y también emite la solicitud de cancelación para todas las copias de ese token.

CancellationToken

Tipo de valor ligero pasado a uno o varios agentes de escucha, normalmente como un parámetro de método. Los agentes de escucha supervisan el valor de la propiedad IsCancellationRequested del token mediante sondeo, devolución de llamada o identificador de espera.

OperationCanceledException

Las sobrecargas del constructor de esta excepción aceptan CancellationToken como parámetro. Los agentes de escucha pueden generar esta excepción para comprobar el origen de la cancelación y notificar a otros que ha respondido a una solicitud de cancelación.

El nuevo modelo de cancelación se integra en .NET Framework en varios tipos. Los más importantes son System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> y System.Linq.ParallelEnumerable. Le recomendamos usar este nuevo modelo de cancelación para todo el código de biblioteca y aplicación nuevo.

En el ejemplo siguiente, el objeto solicitante crea un objeto CancellationTokenSource y, a continuación, pasa su propiedad Token a la operación cancelable. La operación que recibe la solicitud supervisa el valor de la propiedad IsCancellationRequested del token mediante sondeo. Cuando el valor se convierte en true, el agente de escucha puede finalizar de la manera adecuada. En este ejemplo el método simplemente sale, que es lo único necesario en muchos casos.

System_CAPS_noteNota

En el ejemplo se usa el método QueueUserWorkItem para demostrar que el nuevo marco de cancelación es compatible con las API heredadas. Para obtener un ejemplo que usa el nuevo tipo System.Threading.Tasks.Task preferido, vea el artículo sobre How to: Cancel a Task and Its Children.

using System;
using System.Threading;

public class Example
{
   public static void Main()
   {
      // Create the token source.
      CancellationTokenSource cts = new CancellationTokenSource();

      // Pass the token to the cancelable operation.
      ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
      Thread.Sleep(2500);

      // Request cancellation.
      cts.Cancel();
      Console.WriteLine("Cancellation set in token source...");
      Thread.Sleep(2500);
      // Cancellation should have happened, so call Dispose.
      cts.Dispose();
   }

   // Thread 2: The listener
   static void DoSomeWork(object obj)
   {
      CancellationToken token = (CancellationToken)obj;

      for (int i = 0; i < 100000; i++) {
         if (token.IsCancellationRequested)
         {
            Console.WriteLine("In iteration {0}, cancellation has been requested...",
                              i + 1);
            // Perform cleanup if necessary.
            //...
            // Terminate the operation.
            break;
         }
         // Simulate some work.
         Thread.SpinWait(500000);
      }
   }
}
// The example displays output like the following:
//       Cancellation set in token source...
//       In iteration 1430, cancellation has been requested...

En el nuevo marco de cancelación, la cancelación se refiere a las operaciones, no a objetos. La solicitud de cancelación significa que la operación debe detenerse lo antes posible después de realizar cualquier limpieza necesaria. Un token de cancelación debe hacer referencia a una "operación cancelable", independientemente de si esa operación está implementada en su programa. Después de establecer la propiedad IsCancellationRequested del token en true, no puede restablecerse a false. Por lo tanto, los tokens de cancelación no pueden volver a usarse una vez cancelados.

Si necesita un mecanismo de cancelación de objetos, puede basarlo en el mecanismo de cancelación de operaciones mediante una llamada al método CancellationToken.Register, tal como se muestra en el ejemplo siguiente.

using System;
using System.Threading;

class CancelableObject
{
   public string id;

   public CancelableObject(string id)
   {
      this.id = id;
   }

   public void Cancel() 
   { 
      Console.WriteLine("Object {0} Cancel callback", id);
      // Perform object cancellation here.
   }
}

public class Example
{
   public static void Main()
   {
      CancellationTokenSource cts = new CancellationTokenSource();
      CancellationToken token = cts.Token;

      // User defined Class with its own method for cancellation
      var obj1 = new CancelableObject("1");
      var obj2 = new CancelableObject("2");
      var obj3 = new CancelableObject("3");

      // Register the object's cancel method with the token's
      // cancellation request.
      token.Register(() => obj1.Cancel());
      token.Register(() => obj2.Cancel());
      token.Register(() => obj3.Cancel());

      // Request cancellation on the token.
      cts.Cancel();
      // Call Dispose when we're done with the CancellationTokenSource.
      cts.Dispose();
   }
}
// The example displays the following output:
//       Object 3 Cancel callback
//       Object 2 Cancel callback
//       Object 1 Cancel callback

Si un objeto admite más de una operación cancelable simultánea, pase un token independiente como entrada para cada operación cancelable. De este modo, se puede cancelar una operación sin que afecte al resto.

En el delegado de usuario, el implementador de una operación cancelable determina cómo finalizar la operación en respuesta a una solicitud de cancelación. En muchos casos, el delegado de usuario puede realizar simplemente cualquier limpieza necesaria y volver inmediatamente.

Sin embargo, en casos más complejos, es posible que sea necesario que el delegado de usuario notifique al código de biblioteca que se ha producido la cancelación. En estos casos, la manera correcta de finalizar la operación es que el delegado llame al método ThrowIfCancellationRequested, lo que provocará que se genere OperationCanceledException. El código de biblioteca puede detectar esta excepción en el subproceso de delegado de usuario y examinar el token de la excepción para determinar si la excepción indica una cancelación cooperativa o alguna otra situación excepcional.

La clase Task administra OperationCanceledException de esta manera. Para obtener más información, consulte el artículo sobre la Task Cancellation.

Para los cálculos de ejecución prolongada que se repiten, puede escuchar una solicitud de cancelación sondeando periódicamente el valor de la propiedad CancellationToken.IsCancellationRequested. Si su valor es true, el método debe realizar una limpieza y finalizar lo antes posible. La frecuencia óptima de sondeo depende del tipo de aplicación. Es el desarrollador quien determina la mejor frecuencia de sondeo para cualquier programa dado. El sondeo en sí no afecta significativamente al rendimiento. En el ejemplo siguiente se muestra una posible manera de sondeo.

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++) {
      for (int y = 0; y < rect.rows; y++) {
         // Simulating work.
         Thread.SpinWait(5000);
         Console.Write("{0},{1} ", x, y);
      }

      // Assume that we know that the inner loop is very fast.
      // Therefore, checking once per row is sufficient.
      if (token.IsCancellationRequested) {
         // Cleanup or undo here if necessary...
         Console.WriteLine("\r\nCancelling after row {0}.", x);
         Console.WriteLine("Press any key to exit.");
         // then...
         break;
         // ...or, if using Task:
         // token.ThrowIfCancellationRequested();
      }
   }
}

Para obtener un ejemplo más completo, vea el artículo sobre How to: Listen for Cancellation Requests by Polling.

Algunas operaciones se pueden bloquear de forma que no pueden comprobar el valor del token de cancelación de manera oportuna. En estos casos, se puede registrar un método de devolución de llamada que desbloquee el método cuando se reciba una solicitud de cancelación.

El método Register devuelve un objeto CancellationTokenRegistration que se usa específicamente para este propósito. En el ejemplo siguiente se muestra cómo usar el método Register para cancelar una solicitud web asincrónica.

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

class Example
{
   EventHandler externalEvent;

   void Example1()
   {
      CancellationTokenSource cts = new CancellationTokenSource();
      externalEvent +=
         (sender, obj) => { cts.Cancel(); }; //wire up an external requester
      try {
          int val = LongRunningFunc(cts.Token);
      }
      catch (OperationCanceledException) {
          //cleanup after cancellation if required...
          Console.WriteLine("Operation was canceled as expected.");
      }
      finally {
         cts.Dispose();
      }

  }

  private static int LongRunningFunc(CancellationToken token)
  {
      Console.WriteLine("Long running method");
      int total = 0;
      for (int i = 0; i < 100000; i++)
      {
          for (int j = 0; j < 100000; j++)
          {
              total++;
          }
          if (token.IsCancellationRequested)
          { // observe cancellation
              Console.WriteLine("Cancellation observed.");
              throw new OperationCanceledException(token); // acknowledge cancellation
          }
      }
      Console.WriteLine("Done looping");
      return total;
   }

   static void Main()
   {
      Example ex = new Example();

      Thread t = new Thread(new ThreadStart(ex.Example1));
      t.Start();

      Console.WriteLine("Press 'c' to cancel.");
      if (Console.ReadKey(true).KeyChar == 'c')
          ex.externalEvent.Invoke(ex, new EventArgs());

      Console.WriteLine("Press enter to exit.");
      Console.ReadLine();
  }
}

class CancelWaitHandleMiniSnippetsForOverviewTopic
{

  static void CancelByCallback()
  {
      CancellationTokenSource cts = new CancellationTokenSource();
      CancellationToken token = cts.Token;
      WebClient wc = new WebClient();

      // To request cancellation on the token
      // will call CancelAsync on the WebClient.
      token.Register(() => wc.CancelAsync());

      Console.WriteLine("Starting request");
      wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
   }
}

El objeto CancellationTokenRegistration administra la sincronización de subprocesos y garantiza que la devolución de llamada dejará de ejecutarse en un momento concreto en el tiempo.

Para garantizar la capacidad de respuesta del sistema y evitar los interbloqueos, deben seguirse las siguientes directrices al registrar devoluciones de llamada:

  • El método de devolución de llamada debe ser rápido porque se llama sincrónicamente y, por tanto, la llamada a Cancel no devuelve un valor hasta que no se devuelve la devolución de llamada.

  • Si se llama a Dispose mientras se ejecuta la devolución de llamada y se mantiene un bloqueo que está esperando la devolución de llamada, el programa puede causar interbloqueos. Después de que Dispose devuelve un valor, puede liberar todos los recursos requeridos por la devolución de llamada.

  • Las devoluciones de llamada no deben realizar ningún subproceso manual ni uso de o SynchronizationContext en una devolución de llamada. Si una devolución de llamada debe ejecutarse en un subproceso concreto, use el constructor System.Threading.CancellationTokenRegistration que le permite especificar que la clase syncContext de destino es el SynchronizationContext.Current activo. Si se realiza un subproceso manual en una devolución de llamada, puede producirse un interbloqueo.

Para obtener un ejemplo más completo, vea el artículo sobre How to: Register Callbacks for Cancellation Requests.

Cuando una operación cancelable puede bloquearse mientras espera en una primitiva de sincronización como System.Threading.ManualResetEvent o System.Threading.Semaphore, se puede usar la propiedad CancellationToken.WaitHandle para habilitar la operación de espera en el evento y la solicitud de cancelación. El identificador de espera del token de cancelación se señalará en respuesta a una solicitud de cancelación y el método puede usar el valor devuelto del método WaitAny para determinar si era el token de cancelación el que señalaba. A continuación, la operación puede cerrarse o generar OperationCanceledException, según corresponda.

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));

En el nuevo código destinado a .NET Framework 4, System.Threading.ManualResetEventSlim y System.Threading.SemaphoreSlim admiten el nuevo marco de cancelación en sus métodos Wait. CancellationToken puede pasarse al método y, cuando se solicita la cancelación, el evento se activa y genera OperationCanceledException.

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);

Para obtener un ejemplo más completo, vea el artículo sobre How to: Listen for Cancellation Requests That Have Wait Handles.

En algunos casos, un agente de escucha tiene que escuchar varios tokens de cancelación de manera simultánea. Por ejemplo, una operación cancelable puede tener que supervisar un token de cancelación interno además de un token pasado externamente como argumento a un parámetro de método. Para lograr esto, cree un origen de tokens vinculados que pueda combinar dos o más tokens en uno, como se muestra en el ejemplo siguiente.

public void DoWork(CancellationToken externalToken)
{
   // Create a new token that combines the internal and external tokens.
   this.internalToken = internalTokenSource.Token;
   this.externalToken = externalToken;

   using (CancellationTokenSource linkedCts =
           CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
   {
       try {
           DoWorkInternal(linkedCts.Token);
       }
       catch (OperationCanceledException) {
           if (internalToken.IsCancellationRequested) {
               Console.WriteLine("Operation timed out.");
           }
           else if (externalToken.IsCancellationRequested) {
               Console.WriteLine("Cancelling per user request.");
               externalToken.ThrowIfCancellationRequested();
           }
       }
   }
}

Tenga en cuenta que debe llamar a Dispose en el origen de tokens vinculados cuando haya terminado con él. Para obtener un ejemplo más completo, vea el artículo sobre How to: Listen for Multiple Cancellation Requests.

El marco de cancelación unificada hace posible que el código de biblioteca pueda cancelar el código de usuario y que el código de usuario pueda cancelar el código de la biblioteca de manera cooperativa. Una buena cooperación depende de que cada lado siga estas instrucciones:

  • Si el código de biblioteca proporciona operaciones cancelables, también debe proporcionar métodos públicos que acepten un token de cancelación externo para que el código de usuario pueda solicitar la cancelación.

  • Si el código de biblioteca llama al código de usuario, el código de biblioteca debe interpretar OperationCanceledException (externalToken) como cancelación cooperativa y no necesariamente como una excepción de error.

  • Los delegados de usuario deben intentar responder a las solicitudes de cancelación del código de biblioteca de manera adecuada.

System.Threading.Tasks.Task y System.Linq.ParallelEnumerable son ejemplos de clases que siguen estas instrucciones. Para obtener más información, consulte los artículos sobre Task Cancellation y How to: Cancel a PLINQ Query.

Mostrar: