Um guia do programador .NET para CancellationToken

Publicados: 2022-08-23

Às vezes, cancelar é bom. Em muitos dos meus projetos .NET, tive muita motivação para cancelar processos internos e externos. A Microsoft descobriu que os desenvolvedores estavam abordando esse caso de uso comum em uma variedade de implementações complexas e decidiram que deveria haver uma maneira melhor. Assim, um padrão de comunicação de cancelamento comum foi introduzido como CancellationToken , que foi construído usando construções de comunicação entre processos e multithreading de nível inferior. Como parte de minha pesquisa inicial sobre esse padrão - e depois de pesquisar o código-fonte .NET real para a implementação da Microsoft - descobri que CancellationToken pode resolver um conjunto muito mais amplo de problemas: assinaturas em estados de execução de aplicativos, tempo limite de operações usando diferentes gatilhos e comunicações gerais entre processos por meio de sinalizadores.

O caso de uso pretendido do token de cancelamento

CancellationToken foi introduzido no .NET 4 como um meio de aprimorar e padronizar as soluções existentes para operações de cancelamento. Existem quatro abordagens gerais para lidar com o cancelamento que as linguagens de programação populares tendem a implementar:

Matar Diga, não aceite não como resposta Peça educadamente e aceite a rejeição Defina o sinalizador educadamente, deixe-o pesquisar se quiser
Abordagem Parada dura; resolver inconsistências mais tarde Diga-lhe para parar, mas deixe-o limpar as coisas Um pedido direto, mas gentil, para parar Peça para parar, mas não force
Resumo Um caminho infalível para a corrupção e a dor Permite pontos de parada limpos, mas deve parar Permite pontos de parada limpos, mas a solicitação de cancelamento pode ser ignorada O cancelamento é solicitado através de uma bandeira
Pthreads pthread_kill ,
pthread_cancel (assíncrono)
pthread_cancel (modo adiado) n / D Através de uma bandeira
.INTERNET Thread.Abort n / D Thread.Interrupt Através de um sinalizador em CancellationToken
Java Thread.destroy ,
Thread.stop
n / D Thread.interrupt Através de um sinalizador ou Thread.interrupted
Pitão PyThreadState_SetAsyncExc n / D asyncio.Task.cancel Através de uma bandeira
Orientação Inaceitável; evite essa abordagem Aceitável, especialmente quando um idioma não oferece suporte a exceções ou desenrolamento Aceitável se o idioma o suportar Melhor, mas mais como um esforço em grupo
Resumo da Abordagem de Cancelamento e Exemplos de Idioma

CancellationToken reside na categoria final, onde a conversa de cancelamento é cooperativa.

Depois que a Microsoft introduziu o CancellationToken , a comunidade de desenvolvimento rapidamente o adotou, principalmente porque muitas das principais APIs .NET foram atualizadas para usar esses tokens nativamente. Por exemplo, começando com ASP.NET Core 2.0, as ações dão suporte a um parâmetro opcional CancellationToken que pode sinalizar se uma solicitação HTTP foi fechada, permitindo o cancelamento de qualquer operação e evitando assim o uso desnecessário de recursos.

Após um mergulho profundo na base de código .NET, ficou claro que o uso do CancellationToken não se limita ao cancelamento.

CancellationToken sob um microscópio

Ao observar mais de perto a implementação do CancellationToken , vemos que é apenas um sinalizador simples (ou seja, ManualResetEvent ) e a infraestrutura de suporte que fornece a capacidade de monitorar e alterar esse sinalizador. O principal utilitário do CancellationToken está em seu nome, o que sugere que essa é a maneira comum de cancelar operações. Atualmente, qualquer biblioteca, pacote ou framework .NET com operações assíncronas ou de longa duração permite o cancelamento por meio desses tokens.

CancellationToken pode ser acionado manualmente definindo seu sinalizador como "true" ou programando-o para mudar para "true" após um determinado período de tempo. Independentemente de como um CancellationToken é acionado, o código do cliente que está monitorando esse token pode determinar o valor do sinalizador de token por meio de um dos três métodos:

  • Usando um WaitHandle
  • Pesquisando o sinalizador do CancellationToken
  • Informando o código do cliente quando o estado do sinalizador é atualizado por meio de uma assinatura programática

Após mais pesquisas na base de código do .NET, ficou evidente que a equipe do .NET considerou os CancellationTokens úteis em outros cenários não relacionados ao cancelamento. Vamos explorar alguns desses casos de uso avançados e fora da marca, que capacitam os desenvolvedores C# com coordenação multithread e entre processos para simplificar situações complexas.

Tokens de cancelamento para eventos avançados

Ao escrever aplicativos ASP.NET Core, às vezes precisamos saber quando nosso aplicativo foi iniciado ou precisamos injetar nosso código no processo de desligamento do host. Nesses casos, usamos a interface IHostApplicationLifetime (anteriormente IApplicationLifetime ). Essa interface (do repositório do .NET Core) usa CancellationToken para comunicar três eventos principais: ApplicationStarted , ApplicationStopping e ApplicationStopped :

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }

À primeira vista, pode parecer que os CancellationToken não pertencem aqui, especialmente porque estão sendo usados ​​como eventos. No entanto, um exame mais aprofundado revela que esses tokens se encaixam perfeitamente:

  • Eles são flexíveis, permitindo várias maneiras para o cliente da interface ouvir esses eventos.
  • Eles são thread-safe fora da caixa.
  • Eles podem ser criados de diferentes fontes combinando CancellationToken s.

Embora os CancellationToken não sejam perfeitos para todas as necessidades de eventos, eles são ideais para eventos que acontecem apenas uma vez, como início ou término de aplicativos.

CancellationToken para tempo limite

Por padrão, o ASP.NET nos dá muito pouco tempo para desligar. Nos casos em que queremos um pouco mais de tempo, usar a classe HostOptions nos permite alterar esse valor de tempo limite. Abaixo, esse valor de tempo limite é encapsulado em um CancellationToken e alimentado nos subprocessos subjacentes.

O método IHostedService de StopAsync é um ótimo exemplo desse uso:

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }

Como fica evidente na definição da interface IHostedService , o método StopAsync usa um parâmetro CancellationToken . O comentário associado a esse parâmetro comunica claramente que a intenção inicial da Microsoft para CancellationToken era como um mecanismo de tempo limite em vez de um processo de cancelamento.

Na minha opinião, se essa interface existisse antes da existência de CancellationToken , isso poderia ter sido um parâmetro TimeSpan — para indicar por quanto tempo a operação de parada teve permissão para ser processada. Na minha experiência, os cenários de tempo limite quase sempre podem ser convertidos em um CancellationToken com grande utilidade adicional.

Por enquanto, vamos esquecer que sabemos como o método StopAsync é projetado e, em vez disso, pensar em como projetaríamos o contrato desse método. Primeiro vamos definir os requisitos:

  • O método StopAsync deve tentar interromper o serviço.
  • O método StopAsync deve ter um estado de parada normal.
  • Independentemente de um estado de parada normal ser alcançado, um serviço hospedado deve ter um tempo máximo para parar, conforme definido pelo nosso parâmetro timeout.

Ao ter um método StopAsync em qualquer formato, atendemos ao primeiro requisito. Os requisitos restantes são complicados. CancellationToken atende a esses requisitos exatamente usando uma ferramenta de comunicação padrão baseada em sinalizador .NET para capacitar a conversa.

CancellationToken como mecanismo de notificação

O maior segredo por trás do CancellationToken é que é apenas um sinalizador. Vamos ilustrar como CancellationToken pode ser usado para iniciar processos em vez de pará-los.

Considere o seguinte:

  1. Crie uma classe RandomWorker .
  2. RandomWorker deve ter um método DoWorkAsync que executa algum trabalho aleatório.
  3. O método DoWorkAsync deve permitir que um chamador especifique quando o trabalho deve começar.
 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

A classe acima satisfaz os dois primeiros requisitos, deixando-nos com o terceiro. Existem várias interfaces alternativas que podemos usar para acionar nosso trabalhador, como um intervalo de tempo ou um sinalizador simples:

 # With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();

Essas duas abordagens são boas, mas nada é tão elegante quanto usar um CancellationToken :

 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

Este código de cliente de amostra ilustra o poder desse design:

 using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }

O CancellationTokenSource criará nosso CancellationToken nos bastidores e coordenará o acionamento de todos os processos associados. Nesse caso, o processo associado é nosso RandomWorker , que está aguardando para iniciar. Essa abordagem nos permite aproveitar a segurança do encadeamento incorporada à implementação padrão do CancellationToken .

Uma caixa de ferramentas expansiva de token de cancelamento

Esses exemplos demonstram como CancellationToken fornece uma caixa de ferramentas de soluções que são úteis fora de seu caso de uso pretendido. As ferramentas podem ser úteis em muitos cenários que envolvem comunicação baseada em sinalizadores entre processos. Se nos deparamos com tempos limite, notificações ou eventos únicos, podemos recorrer a essa implementação elegante e testada pela Microsoft.

De cima para baixo, as palavras "Gold" (ouro colorido), "Microsoft" e "Partner" (ambas em preto) aparecem seguidas do logotipo da Microsoft.
Como Microsoft Gold Partner, a Toptal é sua rede de elite de especialistas da Microsoft. Construa equipes de alto desempenho com os especialistas de que você precisa - em qualquer lugar e exatamente quando você precisar deles!

Leitura adicional no Blog da Toptal Engineering:

  • .NET Core: Tornando-se selvagem e de código aberto. Microsoft, por que você demorou tanto?!
  • Criando uma API Web ASP.NET com ASP.NET Core
  • Como inicializar e criar projetos .NET