Qual é a maneira correta de cancelar uma operação assíncrona que não aceita um CancellationToken?

Qual é a maneira correta de cancelar o seguinte?

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

Simplesmente chamar tcpListener.Stop() parece resultar em um ObjectDisposedException e o método AcceptTcpClientAsync não aceita uma estrutura CancellationToken .

Estou totalmente ausente de algo óbvio?

Supondo que você não queira chamar o método Stop na class TcpListener , não há solução perfeita aqui.

Se você está bem em ser notificado quando a operação não termina dentro de um determinado período de tempo, mas permitindo que a operação original seja concluída, você pode criar um método de extensão, da seguinte forma:

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

O texto acima é da postagem no blog de Stephen Toub “Como faço para cancelar operações assíncronas não canceláveis?” .

A advertência aqui é repetitiva, isso não cancela a operação, porque não há uma sobrecarga do método AcceptTcpClientAsync que recebe um CancellationToken , ele não pode ser cancelado.

Isso significa que, se o método de extensão indicar que um cancelamento ocorreu, você está cancelando a espera no retorno de chamada da Task original, não cancelando a operação em si.

Para esse fim, é por isso que WithCancellation o método de WithCancellation para WithWaitCancellation para indicar que você está cancelando a espera , não a ação real.

A partir daí, é fácil usar em seu 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. } 

Observe que você terá que encapsular a chamada para o seu cliente para capturar a instância OperationCanceledException lançada se a espera for cancelada.

Eu também joguei uma captura AggregateException como exceções são quebradas quando lançadas a partir de operações assíncronas (você deve testar por si mesmo neste caso).

Isso deixa a questão de qual abordagem é uma abordagem melhor em face de ter um método como o método Stop (basicamente, qualquer coisa que destrua tudo, independentemente do que está acontecendo), o que, é claro, depende de suas circunstâncias.

Se você não está compartilhando o recurso que está esperando (neste caso, o TcpListener ), então provavelmente seria um uso melhor dos resources para chamar o método de abortar e engolir quaisquer exceções que vêm de operações que você está esperando no (você terá que virar um pouco quando você chamar parar e monitorar esse bit nas outras áreas que você está esperando em uma operação). Isso adiciona alguma complexidade ao código, mas se você estiver preocupado com a utilização de resources e a limpeza o mais rápido possível, e essa opção estiver disponível para você, esse é o caminho a seguir.

Se a utilização de resources não for um problema e você estiver confortável com um mecanismo mais cooperativo, e você não estiver compartilhando o recurso, usar o método WithWaitCancellation é bom. Os profissionais aqui são de que é um código mais limpo e mais fácil de manter.

Embora a resposta da casperOne esteja correta, há uma implementação potencial mais limpa no método de extensão WithCancellation (ou WithWaitCancellation ) que atinge os mesmos objectives:

 static Task WithCancellation(this Task task, CancellationToken cancellationToken) { return task.IsCompleted ? task : task.ContinueWith( completedTask => completedTask.GetAwaiter().GetResult(), cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } 
  • Primeiro, temos uma otimização de caminho rápido, verificando se a tarefa já foi concluída.
  • Então, simplesmente registramos uma continuação na tarefa original e passamos o parâmetro CancellationToken .
  • A continuação extrai o resultado da tarefa original (ou exceção, se houver) de forma síncrona, se possível ( TaskContinuationOptions.ExecuteSynchronously ) e usando um thread ThreadPool se não ( TaskScheduler.Default ), enquanto observa o CancellationToken para cancelamento.

Se a tarefa original for concluída antes que o CancellationToken seja cancelado, a tarefa retornada armazenará o resultado, caso contrário, a tarefa será cancelada e lançará uma TaskCancelledException quando for aguardada.