Async/Await: Para que serve o CancellationToken?
Tags: csharp, async, threads, programming
Published: 2023-11-08 22:38:31

<p>
<!--
https://www.buymeacoffee.com/angelobelchior
Se gostou do conteúdo, que tal me pagar um café? Clica aí em 'Buy me a Coffee'
-->
</p>

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"](https://www.sympla.com.br/evento-online/apis-robustas-com-asp-net-core-8-entity-framework-docker-e-redis/2315443?referrer=www.vemcodar.com.br) 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/](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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w07kz66twufxtl4itruu.jpeg)

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](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken?view=net-7.0) é 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.

```csharp
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();
```
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:

```csharp
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++;
    }
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ada825wruuhmx1qowbv1.png)

Em seguida temos o uso do `ContinueWith`. No [post anterior](https://dev.to/angelobelchior/por-debaixo-do-capo-taskresult-e-a-morte-dos-pandas-de-madagascar-5071) 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`:

```bash
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)
```
Nesse caso a decisão é sua: [callbackhell](http://callbackhell.com/) ou `try/catch`.

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

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

Para acessar o código fonte desse exemplo clique [aqui](https://github.com/angelobelchior/CancellationTokenSample).

---

**"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 `CancellationToken`em 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):

```csharp
[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;
    }
}
```

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_
```bash
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) 
```
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:

{% youtube mF_G898-rOY %}

Para acessar o código fonte desse exemplo clique [aqui](https://github.com/angelobelchior/CancellationTokenApi)!

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](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping#:~:text=Object%E2%80%93relational%20mapping%20%28ORM,,from%20within%20the%20programming%20language.) ou se você utiliza [ADO.NET](https://learn.microsoft.com/pt-br/dotnet/framework/data/adonet/) 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!