¿Cuál es la forma correcta de cancelar una operación asincrónica que no acepta un CancelToken?

¿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 WithWaitCancellation( this Task task, CancellationToken cancellationToken) { // The tasck completion source. var tcs = new TaskCompletionSource(); // Register with the cancellation token. using(cancellationToken.Register( s => ((TaskCompletionSource)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 callback 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.

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 objectives:

 static Task WithCancellation(this Task 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 subproceso ThreadPool if not ( TaskScheduler.Default ) al observar el CancellationToken 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 .