c# - how - ¿Cuál es la forma correcta de cancelar una operación asincrónica que no acepta un CancelToken?
how to use cancellationtoken c# (2)
Si bien la respuesta de casperOne es correcta, existe una implementación de potencial más limpio para el método de extensión WithCancellation
(o WithWaitCancellation
) que logra los mismos objetivos:
static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
return task.IsCompleted
? task
: task.ContinueWith(
completedTask => completedTask.GetAwaiter().GetResult(),
cancellationToken,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
- Primero tenemos una optimización de ruta rápida comprobando si la tarea ya se ha completado.
- Luego, simplemente registramos una continuación de la tarea original y pasamos el parámetro
CancellationToken
. - La continuación extrae el resultado de la tarea original (o la excepción si hay uno) de forma síncrona si es posible (
TaskContinuationOptions.ExecuteSynchronously
) y el uso de un subprocesoThreadPool
if not (TaskScheduler.Default
) al observar elCancellationToken
para la cancelación.
Si la tarea original finaliza antes de cancelar CancellationToken
, la tarea devuelta almacena el resultado; de lo contrario, la tarea se cancela y arrojará una TaskCancelledException
cuando se la TaskCancelledException
.
¿Cuál es la forma correcta de cancelar lo siguiente?
var tcpListener = new TcpListener(connection);
tcpListener.Start();
var client = await tcpListener.AcceptTcpClientAsync();
Simplemente llamando a tcpListener.Stop()
parece dar como resultado una ObjectDisposedException
y el método AcceptTcpClientAsync
no acepta una estructura CancellationToken
.
¿Me estoy perdiendo algo obvio?
Suponiendo que no desea llamar al método Stop
en la clase TcpListener
, no hay una solución perfecta aquí.
Si está bien que se le notifique cuando la operación no se completa dentro de un cierto período de tiempo, pero permitiendo que se complete la operación original, entonces puede crear un método de extensión, como sigue:
public static async Task<T> WithWaitCancellation<T>(
this Task<T> task, CancellationToken cancellationToken)
{
// The tasck completion source.
var tcs = new TaskCompletionSource<bool>();
// Register with the cancellation token.
using(cancellationToken.Register(
s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
// If the task waited on is the cancellation token...
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
// Wait for one or the other to complete.
return await task;
}
Lo anterior es de la publicación de blog de Stephen Toub "¿Cómo puedo cancelar operaciones asíncronas no cancelables?" .
La advertencia aquí AcceptTcpClientAsync
la AcceptTcpClientAsync
repetir, esto en realidad no cancela la operación, porque no hay una sobrecarga del método AcceptTcpClientAsync
que toma un CancellationToken
, no se puede cancelar.
Esto significa que si el método de extensión indica que se produjo una cancelación, está cancelando la espera en la devolución de llamada de la Task
original, no cancelando la operación en sí.
Con ese fin, es por eso que he cambiado el nombre del método de WithCancellation
a WithWaitCancellation
para indicar que estás cancelando la espera , no la acción real.
A partir de ahí, es fácil de usar en tu código:
// Create the listener.
var tcpListener = new TcpListener(connection);
// Start.
tcpListener.Start();
// The CancellationToken.
var cancellationToken = ...;
// Have to wait on an OperationCanceledException
// to see if it was cancelled.
try
{
// Wait for the client, with the ability to cancel
// the *wait*.
var client = await tcpListener.AcceptTcpClientAsync().
WithWaitCancellation(cancellationToken);
}
catch (AggregateException ae)
{
// Async exceptions are wrapped in
// an AggregateException, so you have to
// look here as well.
}
catch (OperationCancelledException oce)
{
// The operation was cancelled, branch
// code here.
}
Tenga en cuenta que deberá cerrar la llamada para que su cliente capture la instancia de OperationCanceledException
lanzada si se cancela la espera.
También lancé una captura de excepción de AggregateException
ya que las excepciones se envuelven cuando se lanzan desde operaciones asíncronas (en este caso, debe probarlo).
Eso deja la pregunta de qué enfoque es un mejor enfoque frente a tener un método como el método Stop
(básicamente, cualquier cosa que destruya violentamente todo, independientemente de lo que esté sucediendo), que por supuesto depende de tus circunstancias.
Si no está compartiendo el recurso que está esperando (en este caso, el TcpListener
), probablemente sea mejor utilizar los recursos para llamar al método de interrupción y evitar cualquier excepción que provenga de las operaciones que está esperando. (tendrá que voltear un poco cuando llame a detener y controlar ese bit en las otras áreas que está esperando en una operación). Esto agrega cierta complejidad al código, pero si le preocupa la utilización de los recursos y la limpieza tan pronto como sea posible, y esta opción está disponible para usted, este es el camino a seguir.
Si la utilización de los recursos no es un problema y te sientes cómodo con un mecanismo más cooperativo, y no estás compartiendo el recurso, entonces usar el método WithWaitCancellation
está bien. Los pros aquí son que es un código más limpio y más fácil de mantener.