Async/Await: Para que serve o CancellationToken?

angelobelchior

Angelo Belchior

Posted on November 8, 2023

Async/Await: Para que serve o CancellationToken?

Imagine o seguinte cenário:

Abro minha aplicação e vou até a opção "Relatório de Vendas". Eu configuro os filtros de pesquisa e clico no botão "Consultar". Um processo assíncrono é iniciado e a tela mostra uma animação indicando que a consulta está sendo executada.
Porém, não me dei conta de que pedi para que fossem mostrados os últimos 24 meses de vendas e isso acaba demorando muito. A tela fica bloqueada enquanto a consulta é executada.
Quero refazer a consulta mas preciso aguardar o fim do processamento. O que eu faço?


Antes de continuar, #VemCodar com a gente!!

Tá afim de criar APIs robustas com .NET?

Feito de dev para dev, direto das trincheiras, por quem coloca software em produção há mais de 20 anos, o curso "APIs robustas com ASP.NET Core 8, Entity Framework, Docker e Redis" traz de maneira direta, sem enrolação, tudo que você precisa saber para construir suas APIs de tal forma que elas sejam seguras, performáticas, escaláveis e, acima de tudo, dentro de um ambiente de desenvolvimento totalmente configurado utilizando Docker e Docker Compose.

Não fique de fora! Dê um Up na sua carreira!

O treinamento acontecerá nos dias 27/02, 28/02 e 29/02, online, das 19.30hs às 22.30hs

Acesse: https://vemcodar.com.br/


Imagem do Gandalf, do Senhor dos Anéis, criado por J.R.R Tolkien com um livro na mão escrito cancelled

Provavelmente você já se deparou com essa situação, certo? Esse é um típico cenário onde necessitamos interromper um processamento assíncrono.

E qual seria a melhor maneira de se fazer isso?

Vem comigo que eu te explico!

O que é o Cancellation Token?

O Cancellation Token é uma estrutura que permite notificar uma ou mais tasks de que elas devem ser interrompidas. Ele faz parte do namespace System.Threading, e é usado para gerenciar a interrupção de tarefas paralelas ou assíncronas.

Existem inúmeros cenários onde podemos (e devemos) usar o Cancellation Token. Além do que foi citado acima como exemplo, ainda podemos ter situações em que definimos um limite de tempo para que uma operação assíncrona finalize e caso ela demore mais do que o previsto, uma notificação é enviada forçando sua interrupção, o famoso Timeout!

Outro ponto muito utilizado é em processos que envolvem I/O, comunicação de rede, consulta a banco de dados ou qualquer operação que seja demorada.

Como o Cancellation Token funciona

O seu funcionamento é muito simples. Primeiro criamos uma fonte de cancelamento - CancellationTokenSource - que contém um CancellationToken. Esse token vai ser repassado para cada tarefa assíncrona.
Quando for necessário interromper os processos, invocamos o método CancellationTokenSource.Cancel(). A partir desse momento, toda e qualquer task que estiver com o Token (CancellationToken) vai ser notificada e deverá encerrar seu processamento.

Abaixo segue um exemplo simples, e em seguida um exemplo que demonstra a utilização de um timeout, onde uma tarefa vai ser interrompida caso demore mais do que 10 segundos para finalizar.



var cancellationTokenSource = new CancellationTokenSource();

Console.WriteLine("Para cancelar pressione Enter/Return...");

var task = Task.Run(() =>
{
    for (var i = 0; i < 10; i++)
    {
        if (cancellationTokenSource.Token.IsCancellationRequested)
        {
            Console.WriteLine("Operação cancelada.");
            return;
        }

        // Simula alguma operação demorada
        Thread.Sleep(1000);
        Console.WriteLine($"Iteração {i + 1}");
    }

    Console.WriteLine("Operação concluída com êxito.");
});

Console.ReadLine();

cancellationTokenSource.Cancel();

await task;

Console.ReadLine();


Enter fullscreen mode Exit fullscreen mode

Explicando: Primeiro criamos um CancellationTokenSource, em seguida criamos uma uma tarefa assíncrona usando Task.Run.

Esse processo simula uma operação "lenta". Caso o usuário pressione Enter/Return, é invocado o método cancellationTokenSource.Cancel(); e o processo é notificado através da propriedade cancellationTokenSource.Token.IsCancellationRequested forçando sua interrupção. Simples assim.

Note que o CancellationToken apenas detém a informação de que foi solicitada uma interrupção do processo. Quem tem que garantir que o processo deva ser interrompido da maneira correta é quem recebe o token.

Abaixo trago um novo exemplo, só que dessa vez vamos colocar um limite de tempo para a execução do processo:



using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(10000);
await ProcessoDemorado(cancellationTokenSource.Token).ContinueWith(task =>
{
    if (task.IsCanceled)
    {
        Console.WriteLine("Processo cancelado por Timeout...");
        return;
    }
});

Console.WriteLine("Fim");
Console.Read();

static async Task ProcessoDemorado(CancellationToken cancellationToken)
{
    var i = 0;
    while(true)
    {
        Console.WriteLine($"{i + 1} segundo{(i == 0 ? "" : "s")}");
        await Task.Delay(1000, cancellationToken);
        i++;
    }
}


Enter fullscreen mode Exit fullscreen mode

Bora entender: Começamos criando um CancellationTokenSource e utilizamos o método CancelAfter para agendar um cancelamento após 10.000 milisegundos (10 segundos para os íntimos...).

Em seguida é invocado o método assíncrono ProcessoDemorado e repassamos o Token. É aqui onde precisamos ter atenção! Se existe um CancellationToken precisamos repassá-lo sempre para os métodos que o esperam como argumento, afinal, podemos ter inúmeros processos assíncronos que necessitam ser interrompidos caso o Token receba sinal de cancelamento.

Eu gosto de usar o Rider porque ele tem analisadores que me alertam caso eu não repasse o token:

IDE Rider alertando que o CancellationToken deveria ser passado para o método Task.Delay

Em seguida temos o uso do ContinueWith. No post anterior eu o citei como uma alternativa ao uso do .Result, porém nesse caso utilizamos em conjunto do await.

Eu fiz dessa maneira para poder ter controle do cancelamento da task fazendo a validação if (task.IsCanceled){...}.
Essa abordagem não é obrigatória, porém, caso não a utilize, receberás uma exception:



Unhandled exception. System.Threading.Tasks.TaskCanceledException: **A task was canceled.**
   at Program.<<Main>$>g__ProcessoDemorado|0_0(CancellationToken cancellationToken) in /Volumes/SSD/Git/CancellationTokenSample/CancellationTokenSample/Program.cs:line 15
   at Program.<Main>$(String[] args) in /Volumes/SSD/Git/CancellationTokenSample/CancellationTokenSample/Program.cs:line 4
   at Program.<Main>(String[] args)


Enter fullscreen mode Exit fullscreen mode

Nesse caso a decisão é sua: callbackhell ou try/catch.

Reforço aqui as palavras do
David Fowler no X: "Please remember to dispose of your CancellationTokenSource. #asynctip" / X (twitter.com)

Sempre utilizem o CancellationTokenSource com o using para que aconteça o Dispose.

Para acessar o código fonte desse exemplo clique aqui.


"Ok Angelo, legal. Entendi tudo. Só não consegui entender qual é a utilidade disso no dia a dia, em produção, entre becos e esquinas da programação de rua... Onde eu aplico isso?".

Imaginei. Exemplos precisam ser didáticos. Eu quis apenas apresentar a ideia, mas agora quero mostrar, de fato, onde podemos aplicar o CancellationTokenem produção, em partes da aplicação que, com certeza, vão salvar o dia.

Voltemos ao começo do post:

Abro minha aplicação e vou até a opção "Relatório de Vendas". Eu configuro os filtros de pesquisa e clico no botão "Consultar". Um processo assíncrono é iniciado e a tela mostra uma animação indicando que a consulta está sendo executada.
Porém, não me dei conta de que pedi para que sejam mostrados os últimos 24 meses de vendas e isso acaba demorando muito. A tela fica bloqueada enquanto a consulta é executada.

Esse é um cenário comum, do mundo real, certo?

Tecnicamente falando, nesse exemplo temos uma API que recebe os parâmetros de filtro e passa para uma camada de repositório (ou seja lá como você chame) onde será feita a consulta no banco de dados.

Abaixo um exemplo simulando esse processo (sem se preocupar em acessar banco de dados):



[HttpGet]
public async Task<IEnumerable<Sale>> Get(
    DateOnly startDate, 
    DateOnly endDate, 
    CancellationToken cancellationToken)
{
    try
    {
        // Simula uma operação lenta
        _logger.LogInformation("Simula uma operação lenta");
        await Task.Delay(5000, cancellationToken);

        return new List<Sale>
        {
            new (1, new DateOnly(2023, 01, 01), 100),
            new (2, new DateOnly(2023, 01, 02), 200),
            new (3, new DateOnly(2023, 01, 03), 300),
            new (4, new DateOnly(2023, 01, 04), 400),
            new (5, new DateOnly(2023, 01, 05), 500),
        };
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Esse erro ocorreu pelo fato de o usuário ter cancelado a requisição");
        throw;
    }
}


Enter fullscreen mode Exit fullscreen mode

Logo de cara podemos notar que a Action Get recebe alguns parâmetros, dentre eles o CancellationToken.
O Asp.Net automágicamente repassa o CancellationToken que foi criado no momento da requisição (A requisição nasce junto com um CancellationTokenSource.
Sendo assim, logo de cara temos esse token e podemos devemos repassá-lo a todo e qualquer método que o use como argumento.

Caso haja uma interrupção no processo, teremos a seguinte exception



fail: CancellationTokenApi.Controllers.SalesReportController[0]
      Esse erro ocorreu pelo fato de o usuário ter cancelado a requisição
      System.Threading.Tasks.TaskCanceledException: A task was canceled.
         at CancellationTokenApi.Controllers.SalesReportController.Get(DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken) 


Enter fullscreen mode Exit fullscreen mode

Daí pra frente temos a garantia de que, caso o usuário cancele a requisição (apertando um botão cancelar, ou perdendo conectividade com o servidor), essa mensagem vai ser propagada e todos os componentes que tiverem o token vão forçar a interrupção do processamento.

Abaixo segue um vídeo mostrando o exemplo acima na prática:

Para acessar o código fonte desse exemplo clique aqui!

No exemplo acima eu coloquei um Task.Delay para simular um processamento demorado, mas imagine que poderia ser uma consulta ao banco de dados. E nesse ponto tanto faz se você utiliza Entity Framework, Dapper ou qualquer ORM ou se você utiliza ADO.NET na unha. Todas essas alternativas disponibilizam métodos assíncronos que esperam um CancellationToken como parâmetro.

Dessa forma, podemos interromper a consulta ao banco de dados, fazendo com que o processamento, memória, I/O do disco e/ou rede NÃO SEJAM DESPERDIÇADOS em uma requisição que foi cancelada.

Independentemente de qualquer cenário, nossa obrigação é sempre zelar pelo baixo consumo de recursos da máquina. Isso é o dever de toda pessoa desenvolvedora.

Essa abordagem é muito simples de se implementar e com certeza vai ajudar a melhorar a performance do seu sistema!

Era isso :)

Muito Obrigado e até a próxima!

💖 💪 🙅 🚩
angelobelchior
Angelo Belchior

Posted on November 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related