Cancelación

.NET Framework versión 4 presenta un nuevo 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 operación cancelable, por ejemplo creando un nuevo subproceso o tarea, pasa el token a la operación. Esa operación puede 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 la operación deje de hacer lo que está haciendo. 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. En la siguiente ilustración se muestra la relación entre un origen de token y todas las copias de su token.

CancellationTokenSource y CancellationTokens

El nuevo modelo de cancelación simplifica la creación de aplicaciones y bibliotecas preparadas para la cancelación, y admite las siguientes características:

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

  • Solicitar es distinto que escuchar. Un objeto que invoca una operación cancelable puede controlar cuándo se solicita la cancelación (si ocurre alguna vez).

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

  • Un agente de escucha puede escuchar varios tokens simultáneamente combinándolos en un token vinculado.

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

  • Se puede notificar a los agentes de escucha de las solicitudes de cancelación sondeando, registrando la devolución de llamada o esperando en identificadores de espera.

Nuevos tipos de cancelación

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

Nombre de tipo

Descripción

CancellationTokenSource

Objeto que 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 más agentes de escucha, normalmente como un parámetro del método. Los agentes de escucha supervisan el valor de la propiedad IsCancellationRequested del token sondeando, devolviendo la llamada o en un identificador de espera.

OperationCanceledException

Las nuevas sobrecargas de esta excepción aceptan CancellationToken como parámetro de entrada. Los agentes de escucha pueden producir opcionalmente 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. Se recomienda usar este nuevo modelo de cancelación para todo el código de biblioteca y aplicación nuevo.

Ejemplo de código

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 todo lo necesario en muchos casos.

NotaNota

En el ejemplo se usa el método QueueUserWorkItem para mostrar que el nuevo marco de cancelación es compatible con las API heredadas.Para obtener un ejemplo donde se usa el nuevo tipo preferido System.Threading.Tasks.Task, vea Cómo: Cancelar una tarea y sus elementos secundarios.

Shared Sub CancelWithThreadPoolMiniSnippet()


    'Thread 1: The Requestor
    ' Create the token source.
    Dim cts As New CancellationTokenSource()

    ' Pass the token to the cancelable operation.
    ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)

    ' Request cancellation by setting a flag on the token.
    cts.Cancel()
    ' end block
End Sub

'Thread 2: The Listener
Shared Sub DoSomeWork(ByVal obj As Object)

    Dim token As CancellationToken = CType(obj, CancellationToken)
    For i As Integer = 0 To 1000000

        ' Simulating work.
        Thread.SpinWait(5000000)

        If token.IsCancellationRequested Then

            ' Perform cleanup if necessary.
            '...
            ' Terminate the operation.
            Exit For
        End If
    Next
End Sub
static void CancelWithThreadPoolMiniSnippet()
{

    //Thread 1: The Requestor
    // Create the token source.
    CancellationTokenSource cts = new CancellationTokenSource();

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

    // Request cancellation by setting a flag on the token.
    cts.Cancel();
}

//Thread 2: The Listener
static void DoSomeWork(object obj)
{
    CancellationToken token = (CancellationToken)obj;
    for (int i = 0; i < 100000; i++)
    {
        // Simulating work.
        Thread.SpinWait(5000000);

        if (token.IsCancellationRequested)
        {
            // Perform cleanup if necessary.
            //...
            // Terminate the operation.
            break;
        }
    }
}

Cancelación de operaciones frente a cancelación de objetos

En el nuevo marco de cancelación, la cancelación se refiere a operaciones, no a objetos. La solicitud de cancelación significa que la operación se debe detener lo antes posible una vez realizada cualquier limpieza necesaria. Un token de cancelación debe hacer referencia a una "operación cancelable"; sin embargo, esa operación se puede implementar en su programa. Una vez establecida en true la propiedad IsCancellationRequested del token, no se puede restablecer a false. Por tanto, los tokens de cancelación no se pueden reutilizar una vez cancelados.

Si necesita un mecanismo de cancelación de objetos, puede basarlo en el mecanismo de cancelación de operaciones, como se muestra en el ejemplo siguiente.

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token

' User defined Class with its own method for cancellation
Dim obj1 As New MyCancelableObject()
Dim obj2 As New MyCancelableObject()
Dim obj3 As New MyCancelableObject()

' Register the object's cancel method with the token's
' cancellation request.
token.Register(Sub() obj1.Cancel())
token.Register(Sub() obj2.Cancel())
token.Register(Sub() obj3.Cancel())

' Request cancellation on the token.
cts.Cancel()
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();

// 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();

Si un objeto admite más de una operación cancelable simultánea, pase un token diferente como entrada a cada operación cancelable distinta. De esa forma, se puede cancelar una operación sin afectar a las demás.

Realizar escuchas y responder a solicitudes de cancelación

El implementador de una operación cancelable determina, en el delegado de usuario, 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 podría ser necesario que un delegado de usuario notificara 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 a ThrowIfCancellationRequested(), que producirá OperationCanceledException. Las nuevas sobrecargas de esta excepción en .NET Framework versión 4 toman CancellationToken como argumento. 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 controla OperationCanceledException de esta forma. Para obtener más información, vea Cancelación de tareas.

Realizar escuchas mediante sondeo

En el caso de cálculos de ejecución prolongada que usan bucles o recorridos, puede escuchar una solicitud de cancelación sondeando periódicamente el valor de la propiedad CancellationToken.IsCancellationRequested. Si el valor es true, el método debe limpiar y finalizar lo más rápidamente 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 propiamente dicho no afecta al rendimiento considerablemente. En el ejemplo siguiente se muestra una forma posible de sondeo.

    Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)

        For x As Integer = 0 To rect.columns

            For y As Integer = 0 To rect.rows

                ' Simulating work.
                Thread.SpinWait(5000)
                Console.Write("0' end block,1' end block ", x, y)
            Next

            ' Assume that we know that the inner loop is very fast.
            ' Therefore, checking once per row is sufficient.
            If token.IsCancellationRequested = True Then

                ' Cleanup or undo here if necessary...
                Console.WriteLine("\r\nCancelling after row 0' end block.", x)
                Console.WriteLine("Press any key to exit.")
                ' then...
                Exit For
                ' ...or, if using Task:
                ' token.ThrowIfCancellationRequested()
            End If
        Next
    End Sub

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 Cómo: Realizar escuchas de solicitudes mediante sondeo.

Realizar escuchas registrando una devolución de llamada

Algunas operaciones se pueden bloquear de forma que no puedan comprobar el valor del token de cancelación a tiempo. En estos casos, 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 fin. En el ejemplo siguiente se muestra cómo usar el método Register para cancelar una solicitud web asincrónica.

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
Dim wc As New WebClient()

' To request cancellation on the token
' will call CancelAsync on the WebClient.
token.Register(Sub() wc.CancelAsync())

Console.WriteLine("Starting request")
wc.DownloadStringAsync(New Uri("https://www.contoso.com"))
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("https://www.contoso.com"));

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

Para asegurar que el sistema responde y para evitar interbloqueos, es preciso tener en cuenta las siguientes pautas a la hora de registrar las devoluciones de llamada:

  • El método de devolución de llamada debe ser rápido porque se invoca de forma sincrónica, por lo que la llamada a Cancel no devuelve ningún valor hasta que la devolución de llamada devuelva un valor.

  • Si llama a Dispose mientras se ejecuta la devolución de llamada, y mantiene un bloqueo que la devolución de llamada está esperando, se puede producir un interbloqueo en el programa. Después de que Dispose devuelva un valor, se podrán liberar todos los recursos que la devolución de llamada necesite.

  • No se deben realizar subprocesos manuales ni usar SynchronizationContext en las devoluciones de llamada. Si una devolución de llamada debe ejecutarse en un subproceso concreto, utilice el constructor System.Threading.CancellationTokenRegistration, que permite especificar que la clase syncContext de destino es la propiedad SynchronizationContext.Current activa. Si se realizan subprocesos manuales en una devolución de llamada, se puede producir un interbloqueo.

Para obtener un ejemplo más completo, vea Cómo: Registrar devoluciones de llamadas de solicitudes de cancelación.

Realizar escuchas mediante un identificador de espera

Cuando una operación cancelable se puede bloquear mientras espera en un primitiva de sincronización como System.Threading.ManualResetEvent o System.Threading.Semaphore, puede usar la propiedad CancellationToken.WaitHandle para permitir que la operación espere tanto en el evento como en la solicitud de cancelación. El identificador de espera del token de cancelación se señalizará como respuesta a una solicitud de cancelación y el método puede emplear el valor devuelto del método WaitAny para determinar si se señalizó el token de cancelación. A continuación, la operación puede salir simplemente o producir OperationCanceledException, según corresponda.

' Wait on the event if it is not signaled.
Dim myWaitHandle(2) As WaitHandle
myWaitHandle(0) = mre
myWaitHandle(1) = token.WaitHandle
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(myWaitHandle, _
                        New TimeSpan(0, 0, 20))
// 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 versión 4, tanto System.Threading.ManualResetEventSlim como System.Threading.SemaphoreSlim admiten el nuevo marco de cancelación en sus métodos Wait. Puede pasar CancellationToken al método y, cuando se solicite la cancelación, el evento se reactivará y producirá una excepción OperationCanceledException.

Try

' mres is a ManualResetEventSlim
  mres.Wait(token)
Catch e As 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("Canceled while waiting.")
    Throw
End Try

 ' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)
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 Cómo: Realizar escuchas de solicitudes de cancelación cuando tienen controladores de espera.

Realizar escuchas en varios tokens simultáneamente

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

Public Sub DoWork(ByVal externalToken As CancellationToken)

    ' Create a new token that combines the internal and external tokens.
    Dim internalToken As CancellationToken = internalTokenSource.Token
    Dim linkedCts As CancellationTokenSource =
    CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
    Using (linkedCts)
        Try
            DoWorkInternal(linkedCts.Token)

        Catch e As OperationCanceledException
            If e.CancellationToken = internalToken Then
                Console.WriteLine("Operation timed out.")

            ElseIf e.CancellationToken = externalToken Then
                Console.WriteLine("Canceled by external token.")
                externalToken.ThrowIfCancellationRequested()
            End If

        End Try
    End Using
End Sub
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 token vinculado cuando haya terminado con él. Para obtener un ejemplo más completo, vea Cómo: Realizar escuchas de varias solicitudes de cancelación.

Cooperación entre código de biblioteca y código de usuario

El marco de cancelación unificada permite que el código de biblioteca cancele código de usuario y que el código de usuario cancele código de 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 a código de usuario, el código de biblioteca debe interpretar OperationCanceledException(externalToken) como una 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 puntual.

System.Threading.Tasks.Task y System.Linq.ParallelEnumerable son ejemplos de clases que siguen estas instrucciones. Para obtener más información, vea Cancelación de tareas y Cómo: Cancelar una consulta PLINQ.

Vea también

Otros recursos

Principios básicos del subprocesamiento administrado