C# - Cancelamento de operações assíncronas - I


Hoje veremos como cancelar operações assíncronas na linguagem C#.

A plataforma .NET usa um modelo unificado para cancelamento cooperativo de operações assíncronas ou síncronas de longa duração. Este modelo é baseado em um objeto leve chamado token de cancelamento.

O objeto que invoca uma ou mais operações canceláveis, por exemplo, criando novas threads ou tarefas e passa o token para cada operação. As operações individuais podem, por sua vez, passar cópias do token para outras operações.

Mais tarde, o objeto que criou o token pode usá-lo para solicitar que as operações parem o que estão fazendo. Apenas o objeto solicitante pode emitir a solicitação de cancelamento, e cada ouvinte é responsável por perceber a solicitação e respondê-la de maneira adequada e oportuna.

O padrão geral para implementar o modelo de cancelamento cooperativo é o seguinte:

Tipos envolvidos na implementação cancelamento

O framework de cancelamento é implementado como um conjunto de tipos relacionados, que estão listados na tabela a seguir.

Nome do tipo DESCRIÇÃO
CancellationTokenSource O objeto que cria um token de cancelamento, e também emite o pedido de cancelamento para todas as cópias desse token.
CancellationToken O tipo de valor leve passado a um ou mais ouvintes, normalmente como um parâmetro de método. Os ouvintes monitoram o valor da propriedade IsCancellationRequested do token por sondagem, retorno de chamada ou identificador de espera.
OperationCanceledException As sobrecargas do construtor desta exceção aceitam CancellationToken como um parâmetro. Os ouvintes podem, opcionalmente, lançar essa exceção para verificar a origem do cancelamento e notificar aos outros que ela respondeu a uma solicitação de cancelamento.

Vejamos a seguir como podemos realizar o cancelamento na prática.

Recursos usados:

Cancelando uma tarefa assíncrona

Vamos iniciar criando um tarefa de longa duração definindo o método OperacaoLongaDuracao() que retorna um Task<int>:

private static Task<int> OperacaoLongaDuracao(int valor)
{
            return Task.Run(() =>
            {
                int resultado = 0;
                for (int i = 0; i < valor; i++)
                {
                    Thread.Sleep(50);
                    resultado += i;
                }
                return resultado;
            });
}

Aqui estou usando Thread.Sleep() que suspende a thread atual por um determinado tempo especificado em milissegundos para simular a longa duração.

A chamada deste método pode ser feita da seguinte forma:

static async Task Main(string[] args)
{
        await ExecutaTaskAsync();
        Console.ReadKey();
}

public static async Task ExecutaTaskAsync()
{
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.WriteLine("Resultado {0}", await OperacaoLongaDuracao(100));
            Console.WriteLine("Tempo gasto..." + stopwatch.Elapsed + "\n");   
 }

Ocorre que aqui não estamos permitindo o cancelamento da tarefa. Vamos implementar o cancelamento usando o padrão geral e o token de cancelamento.

A primeira coisa a fazer é tornar o nosso método de longa duração cancelável passando o CancellationToken para o método como uma parâmetro que aqui eu defini como opcional.

private static Task<int> OperacaoLongaDuracaoCancelavel(int valor, CancellationToken cancellationToken = default)
{
            Task<int> task = null;
            task = Task.Run(() =>
            {
                int resultado = 0;
                for (int i = 0; i < valor; i++)
                {
                    // Verifica se foi solicitado o cancelamento
                    // se foi lança um TaskCanceledException.
                    if (cancellationToken.IsCancellationRequested)
                           throw new TaskCanceledException(task);
                    Thread.Sleep(10);
                    resultado += i;
                }
                return resultado;
            });
            return task;
}

Neste código estamos passando o CancellationToken para o método definindo o valor como default de forma a torná-lo não obrigatório.

A seguir estamos verificando se o método deve ser cancelado lendo a propriedade IsCancellationRequested.(Também é possível usar o método ThrowIfCancellationRequested, que lançará uma OperationCanceledException).

A seguir estamos lançando um exceção do tipo TaskCanceledException(), que que representa uma exceção usada para comunicar o cancelamento da tarefa, .e passando a tarefa.

A seguir podemos chamar o método passando o token de cancelamento com o valor Cancel e estamos usando um bloco try/cacth para tratar a exceção TaskCanceledException:

class Program
{
        private static CancellationTokenSource cancellationTokenSource;
       
        static async Task Main(string[] args)
        {
            try
            {
                await ExecutaTaskCancelamentoAsync();
            }
            catch (TaskCanceledException ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadKey();
        }
        public static async Task ExecutaTaskCancelamentoAsync()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            cancellationTokenSource = new CancellationTokenSource();
            cancellationTokenSource?.Cancel();
            Console.WriteLine($"Executou : {nameof(ExecutaTaskAsync)}");
            Console.WriteLine("Resultado {0}", await OperacaoLongaDuracaoCancelavel(100, cancellationTokenSource.Token));
            Console.WriteLine("Tempo gasto..." + stopwatch.Elapsed + "\n");
        }
...
}

No método Main  estamos usando um bloco try/catch para tratar a exceção que será lançada quando a tarefa for cancelada e chamando o método ExecutaTaskCancelamentoAsync.

Neste método estou criando uma instância de CancellationTokenSource e passando o token com a notificação de cancelamento. Isso vai cancelar a operação de imediato.

A seguir temos o resultado obtido:

Na próxima parte do artigo veremos como cancelar uma operação assíncrona em um período de tempo específico.

E estamos conversados...

Não se turbe o vosso coração; credes em Deus, crede também em mim.
Na casa de meu Pai há muitas moradas. Se assim não fora, eu vo-lo teria dito. Pois vou preparar-vos lugar.

João 14:1,2

Referências:


José Carlos Macoratti