Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
A partir de .NET Framework 4, .NET usa un modelo unificado para la cancelación cooperativa de operaciones sincrónicas asincrónicas o de larga duración. Este modelo se basa en un objeto ligero denominado token de cancelación. El objeto que invoca una o varias operaciones cancelables, por ejemplo, mediante la creación de nuevos subprocesos o tareas, pasa el token a cada operación. A su vez, las operaciones individuales pueden pasar copias del token a otras operaciones. En algún momento posterior, el objeto que creó el token puede usarlo para solicitar que las operaciones detengan lo que están haciendo. Solo el objeto solicitante puede emitir la solicitud de cancelación, y cada escucha es responsable de percibir la solicitud y responder a ella de manera adecuada y oportuna.
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.
Proporcione un mecanismo para que cada tarea o subproceso responda a la cancelación.
Llame al CancellationTokenSource.Cancel método para proporcionar una notificación de cancelación.
Importante
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 ilustración siguiente se muestra la relación entre un origen de token y todas las copias de su token.
El modelo de cancelación cooperativa facilita la creación de bibliotecas y aplicaciones compatibles con cancelación, y admite las siguientes características:
La cancelación es cooperativa y no se impone al oyente. El oyente determina cómo finalizar de manera elegante en respuesta a una solicitud de cancelación.
La solicitud es distinta de la escucha. Un objeto que invoca una operación cancelable puede controlar cuándo se solicita la cancelación, si es que alguna vez se solicita.
El objeto solicitante emite la solicitud de cancelación a todas las copias del token mediante una sola llamada de método.
Un oyente puede oír varios tokens simultáneamente uniéndolos en un token vinculado.
El código de usuario puede observar y responder a las solicitudes de cancelación del código de biblioteca, y el código de biblioteca puede observar y responder a las solicitudes de cancelación del 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.
Tipos de cancelación
El 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 receptores, normalmente como parámetro del 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 oyentes pueden lanzar esta excepción opcionalmente para verificar el origen de la cancelación y notificar a otros que ha respondido a una solicitud de cancelación. |
El modelo de cancelación se integra en .NET en varios tipos. Los más importantes son System.Threading.Tasks.Parallel, System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> y System.Linq.ParallelEnumerable. Se recomienda usar este modelo de cancelación cooperativa para todo el nuevo código de biblioteca y aplicación.
Ejemplo de código
En el ejemplo siguiente, el objeto solicitante crea un CancellationTokenSource objeto y, a continuación, pasa su Token propiedad 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 alcanza true
, la escucha puede finalizar de la manera adecuada. En este ejemplo el método simplemente sale, que es lo único necesario en muchos casos.
Nota:
En el ejemplo se usa el QueueUserWorkItem método para demostrar que el marco de cancelación cooperativa es compatible con las API heredadas. Para obtener un ejemplo en el que se usa el tipo preferido, System.Threading.Tasks.Task, vea Procedimiento para cancelar una tarea y sus elementos secundarios.
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)
{
if (obj is null)
return;
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...
Imports System.Threading
Module Example1
Public Sub Main1()
' Create the token source.
Dim cts As New CancellationTokenSource()
' Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
Thread.Sleep(2500)
' Request cancellation by setting a flag on the token.
cts.Cancel()
Console.WriteLine("Cancellation set in token source...")
Thread.Sleep(2500)
' Cancellation should have happened, so call Dispose.
cts.Dispose()
End Sub
' Thread 2: The listener
Sub DoSomeWork(ByVal obj As Object)
Dim token As CancellationToken = CType(obj, CancellationToken)
For i As Integer = 0 To 1000000
If token.IsCancellationRequested Then
Console.WriteLine("In iteration {0}, cancellation has been requested...",
i + 1)
' Perform cleanup if necessary.
'...
' Terminate the operation.
Exit For
End If
' Simulate some work.
Thread.SpinWait(500000)
Next
End Sub
End Module
' The example displays output like the following:
' Cancellation set in token source...
' In iteration 1430, cancellation has been requested...
Cancelación de Operación frente a Cancelación de Objeto
En el marco de cancelación cooperativa, la cancelación hace referencia a operaciones, no 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", pero esa operación se puede implementar en el programa. Una vez que la propiedad IsCancellationRequested del token se ha establecido en true
, no se puede restablecer a false
. Por lo 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 la operación llamando al CancellationToken.Register método , 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 {id} Cancel callback");
// Perform object cancellation here.
}
}
public class Example1
{
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
Imports System.Threading
Class CancelableObject
Public id As String
Public Sub New(id As String)
Me.id = id
End Sub
Public Sub Cancel()
Console.WriteLine("Object {0} Cancel callback", id)
' Perform object cancellation here.
End Sub
End Class
Module ExampleOb1
Public Sub MainOb1()
Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
' User defined Class with its own method for cancellation
Dim obj1 As New CancelableObject("1")
Dim obj2 As New CancelableObject("2")
Dim obj3 As New CancelableObject("3")
' 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()
' Call Dispose when we're done with the CancellationTokenSource.
cts.Dispose()
End Sub
End Module
' The example displays output like the following:
' 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 a cada operación cancelable distinta. De este modo, se puede cancelar una operación sin afectar a las demás.
Escucha y respuesta a solicitudes de cancelación
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 solo puede realizar cualquier limpieza necesaria y, a continuación, devolver inmediatamente.
Sin embargo, en casos más complejos, podría ser necesario que el delegado de usuario notifique al código de biblioteca que se ha producido la cancelación. En tales casos, la manera correcta de finalizar la operación es que el delegado llame al método ThrowIfCancellationRequested, lo cual ocasionará que se lance una OperationCanceledException. El código de biblioteca puede detectar esta excepción en el subproceso delegado de usuario y examinar el token de la excepción para determinar si la excepción indica la cancelación cooperativa o alguna otra situación excepcional.
La Task clase controla OperationCanceledException de esta manera. Para obtener más información, consulte Cancelación de tareas.
Escuchas mediante sondeo
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 limpiar y finalizar lo antes posible. La frecuencia óptima de sondeo depende del tipo de aplicación. Es necesario que el desarrollador determine la mejor frecuencia de sondeo para cualquier programa determinado. El sondeo en sí no afecta significativamente al rendimiento. En el ejemplo siguiente se muestra una posible manera de sondear.
static void NestedLoops(Rectangle rect, CancellationToken token)
{
for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
// Assume that we know that the inner loop is very fast.
// Therefore, polling once per column in the outer loop condition
// is sufficient.
for (int row = 0; row < rect.rows; row++) {
// Simulating work.
Thread.SpinWait(5_000);
Console.Write("{0},{1} ", col, row);
}
}
if (token.IsCancellationRequested) {
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nOperation canceled");
Console.WriteLine("Press any key to exit.");
// If using Task:
// token.ThrowIfCancellationRequested();
}
}
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
Dim col As Integer
For col = 0 To rect.columns - 1
' Assume that we know that the inner loop is very fast.
' Therefore, polling once per column in the outer loop condition
' is sufficient.
For row As Integer = 0 To rect.rows - 1
' Simulating work.
Thread.SpinWait(5000)
Console.Write("0',1' ", col, row)
Next
Next
If token.IsCancellationRequested = True Then
' Cleanup or undo here if necessary...
Console.WriteLine(vbCrLf + "Operation canceled")
Console.WriteLine("Press any key to exit.")
' If using Task:
' token.ThrowIfCancellationRequested()
End If
End Sub
Para obtener un ejemplo más completo, vea Cómo: Escuchar solicitudes de cancelación mediante sondeo.
Escuchas mediante el registro de una devolución de llamada
Algunas operaciones se pueden bloquear de forma que no puedan comprobar el valor del token de cancelación de forma 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 Register método devuelve un CancellationTokenRegistration objeto que se usa específicamente para este propósito. En el ejemplo siguiente se muestra cómo usar el Register método para cancelar una solicitud web asincrónica.
using System;
using System.Net.Http;
using System.Threading;
class Example4
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
StartWebRequest(cts.Token);
// Cancellation will cause the web
// request to be cancelled.
cts.Cancel();
}
static void StartWebRequest(CancellationToken token)
{
var client = new HttpClient();
token.Register(() =>
{
client.CancelPendingRequests();
Console.WriteLine("Request cancelled!");
});
Console.WriteLine("Starting request.");
client.GetStringAsync(new Uri("http://www.contoso.com"));
}
}
Imports System.Net
Imports System.Net.Http
Imports System.Threading
Class Example4
Private Shared Sub Main4()
Dim cts As New CancellationTokenSource()
StartWebRequest(cts.Token)
' cancellation will cause the web
' request to be cancelled
cts.Cancel()
End Sub
Private Shared Sub StartWebRequest(token As CancellationToken)
Dim client As New HttpClient()
token.Register(Sub()
client.CancelPendingRequests()
Console.WriteLine("Request cancelled!")
End Sub)
Console.WriteLine("Starting request.")
client.GetStringAsync(New Uri("http://www.contoso.com"))
End Sub
End Class
El CancellationTokenRegistration objeto administra la sincronización de subprocesos y garantiza que el callback dejará de ejecutarse en un momento preciso.
Para garantizar la capacidad de respuesta del sistema y evitar interbloqueos, se deben seguir las siguientes directrices al registrar funciones de retrollamada.
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 Cómo: Registro de devoluciones de llamada como solicitudes de cancelación.
Escuchas mediante un identificador de espera
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. Después, la operación puede cerrarse o generar la excepción 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));
' Wait on the event if it is not signaled.
Dim waitHandles() As WaitHandle = {mre, token.WaitHandle}
Dim eventThatSignaledIndex =
WaitHandle.WaitAny(waitHandles, _
New TimeSpan(0, 0, 20))
System.Threading.ManualResetEventSlim y System.Threading.SemaphoreSlim admiten el 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);
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)
Para obtener un ejemplo más completo, vea Cómo: Escucha de solicitudes de cancelación que tienen identificadores de espera.
Escucha de varios tokens simultáneamente
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 ello, cree un origen de token vinculado que pueda combinar dos o más tokens en un token, 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();
}
}
}
}
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
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 Cómo: Escuchar varias solicitudes de cancelación.
Cooperación entre código de biblioteca y código de usuario
El marco de cancelación unificado permite que el código de biblioteca cancele el código de usuario y el código de usuario cancele el código de biblioteca de forma cooperativa. La cooperación sin problemas depende de cada lado siguiendo estas directrices:
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 excepción de error.
Los delegados de usuario deben intentar responder a las solicitudes de cancelación del código de biblioteca de forma oportuna.
System.Threading.Tasks.Task y System.Linq.ParallelEnumerable son ejemplos de clases que siguen estas directrices. Para obtener más información, vea Cancelación de tareas y Cómo: Cancelar una consulta PLINQ.