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 |
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:
- Crie uma classe
RandomWorker
. -
RandomWorker
deve ter um métodoDoWorkAsync
que executa algum trabalho aleatório. - 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.
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