Qual è il modo corretto di annullare un’operazione asincrona che non accetta un metodo di annullamento?

Qual è il modo corretto per cancellare quanto segue?

var tcpListener = new TcpListener(connection); tcpListener.Start(); var client = await tcpListener.AcceptTcpClientAsync(); 

Semplicemente chiamando tcpListener.Stop() sembra risultare in una ObjectDisposedException e il metodo AcceptTcpClientAsync non accetta una struttura di CancellationToken .

Mi manca qualcosa di ovvio?

Supponendo che non si voglia chiamare il metodo Stop sulla class TcpListener , non esiste una soluzione perfetta qui.

Se stai bene con la notifica quando l’operazione non viene completata entro un certo periodo di tempo, ma consenti il ​​completamento dell’operazione originale, puoi creare un metodo di estensione, in questo modo:

 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; } 

Quanto sopra è tratto dal post del blog di Stephen Toub “Come posso annullare le operazioni asincrone non cancellabili?” .

L’avvertimento qui ripete, questo in realtà non annulla l’operazione, perché non c’è un sovraccarico del metodo AcceptTcpClientAsync che accetta un metodo di CancellationToken , non è ansible annullarlo.

Ciò significa che se il metodo di estensione indica che si è verificata una cancellazione, si sta annullando l’attesa sulla richiamata dell’attività originale, non annullando l’operazione stessa.

A tal fine, è per questo che ho rinominato il metodo da WithCancellation a WithWaitCancellation per indicare che si sta annullando l’ attesa , non l’azione effettiva.

Da lì, è facile da usare nel tuo codice:

 // 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. } 

Tieni presente che dovrai ricoprire la chiamata affinché il tuo cliente acquisisca l’istanza OperationCanceledException generata se l’attesa viene annullata.

Ho anche inserito un catch AggregateException quando le eccezioni vengono completate quando vengono generate da operazioni asincrone (in questo caso dovresti provare tu stesso).

Questo lascia la domanda su quale approccio sia un approccio migliore di fronte ad avere un metodo come il metodo Stop (fondamentalmente, tutto ciò che distrugge violentemente tutto, indipendentemente da quello che sta succedendo), che, ovviamente, dipende dalle circostanze.

Se non stai condividendo la risorsa che stai aspettando (in questo caso, TcpListener ), allora probabilmente sarebbe un uso migliore delle risorse per chiamare il metodo di interruzione e ingoiare eventuali eccezioni derivanti da operazioni che stai aspettando su (dovrai girare un po ‘quando chiami stop e monitora quel bit nelle altre aree che stai aspettando su un’operazione). Ciò aggiunge una certa complessità al codice, ma se sei preoccupato dell’utilizzo delle risorse e della pulizia il prima ansible, e questa scelta è a tua disposizione, allora questa è la strada da percorrere.

Se l’utilizzo delle risorse non è un problema e sei a tuo agio con un meccanismo più cooperativo e non stai condividendo la risorsa, allora il metodo WithWaitCancellation va bene. I professionisti qui sono che è un codice più pulito e più facile da mantenere.

Mentre la risposta di casperOne è corretta, esiste una potenziale implementazione più pulita del metodo di estensione WithCancellation (o WithWaitCancellation ) che raggiunge gli stessi obiettivi:

 static Task WithCancellation(this Task task, CancellationToken cancellationToken) { return task.IsCompleted ? task : task.ContinueWith( completedTask => completedTask.GetAwaiter().GetResult(), cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } 
  • Per prima cosa abbiamo un’ottimizzazione del percorso veloce controllando se l’attività è già stata completata.
  • Quindi registriamo semplicemente una continuazione all’attività originale e trasmettiamo il parametro CancellationToken .
  • La continuazione estrae il risultato TaskContinuationOptions.ExecuteSynchronously (o eccezione se ce n’è uno) in modo sincrono se ansible ( TaskContinuationOptions.ExecuteSynchronously ) e utilizzando un thread ThreadPool se non ( TaskScheduler.Default ) durante l’osservazione di CancellationToken per la cancellazione.

Se l’attività originale viene completata prima dell’annullamento di CancellationToken l’attività restituita memorizza il risultato, altrimenti l’attività viene annullata e verrà TaskCancelledException una TaskCancelledException quando è attesa.