Cancelamento em threads gerenciados

A partir do .NET Framework 4, o .NET usa um modelo unificado para cancelamento cooperativo de operações assíncronas ou síncronas de longa execuçã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 novos tópicos ou tarefas, passa o token para cada operação. As operações individuais podem, por sua vez, passar cópias do token para outras operações. Posteriormente, o objeto que criou o token pode usá-lo para solicitar que as operações parem o que estão fazendo. Somente o objeto solicitante pode emitir a solicitação de cancelamento, e cada ouvinte é responsável por perceber a solicitação e respondê-la de forma apropriada e oportuna.

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

  • Instancie um objeto CancellationTokenSource, que gerencia e envia uma notificação de cancelamento para os tokens de cancelamento individuais.

  • Passe o token retornado pela propriedade CancellationTokenSource.Token para cada tarefa ou thread que responde ao cancelamento.

  • Forneça um mecanismo para cada tarefa ou thread responder ao cancelamento.

  • Chame o método CancellationTokenSource.Cancel para fornecer uma notificação de cancelamento.

Importante

A classe CancellationTokenSource implementa a interface IDisposable. Certifique-se de chamar o método CancellationTokenSource.Dispose quando terminar de usar a fonte de token de cancelamento para liberar todos os recursos não gerenciados detidos.

A ilustração a seguir mostra o relacionamento entre uma fonte de token e todas as cópias de seu token.

CancellationTokenSource and cancellation tokens

O modelo de cancelamento cooperativo facilita a criação de aplicativos e bibliotecas com reconhecimento de cancelamento, e ele dá suporte aos seguintes recursos:

  • O cancelamento é cooperativo e não será forçado ao ouvinte. O ouvinte determina como encerrar normalmente em resposta a um pedido de cancelamento.

  • Solicitar é diferente de detectar. Um objeto que invoca uma operação cancelável pode controlar quando (e se) o cancelamento será solicitado.

  • O objeto solicitante emite o pedido de cancelamento para todas as cópias do token usando apenas uma chamada de método.

  • Um ouvinte pode detectar vários tokens simultaneamente juntando-os a um token vinculado.

  • O código do usuário pode notar e responder às solicitações de cancelamento do código da biblioteca e o código da biblioteca pode observar e responder às solicitações de cancelamento do código do usuário.

  • Os ouvintes podem ser notificados sobre solicitações de cancelamento por sondagem, registro de retorno de chamada ou aguardando identificadores de espera.

Tipos de cancelamento

A estrutura de cancelamento é implementada 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.

O modelo de cancelamento está integrado no .NET em vários tipos. Os mais importantes são System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> e System.Linq.ParallelEnumerable. Recomendamos o uso desse modelo de cancelamento cooperativo para todas as novas bibliotecas e códigos de aplicativos.

Exemplo de código

No exemplo a seguir, o objeto solicitante cria um objeto CancellationTokenSource e, em seguida, passa sua propriedade Token para a operação cancelável. A operação que recebe a solicitação monitora o valor da propriedade IsCancellationRequested do token por sondagem. Quando o valor se torna true, o ouvinte pode concluir de qualquer maneira apropriada. Neste exemplo, o método apenas sai, que é tudo necessário em muitos casos.

Observação

O exemplo usa o método QueueUserWorkItem para demonstrar que a estrutura de cancelamento cooperativo é compatível com as APIs herdadas. Para um exemplo que usa o tipo preferido System.Threading.Tasks.Task, consulte Como cancelar uma tarefa e seus filhos.

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Create the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        // Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        Thread.Sleep(2500);

        // Request cancellation.
        cts.Cancel();
        Console.WriteLine("Cancellation set in token source...");
        Thread.Sleep(2500);
        // Cancellation should have happened, so call Dispose.
        cts.Dispose();
    }

    // Thread 2: The listener
    static void DoSomeWork(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken token = (CancellationToken)obj;

        for (int i = 0; i < 100000; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1);
                // Perform cleanup if necessary.
                //...
                // Terminate the operation.
                break;
            }
            // Simulate some work.
            Thread.SpinWait(500000);
        }
    }
}
// The example displays output like the following:
//       Cancellation set in token source...
//       In iteration 1430, cancellation has been requested...
Imports System.Threading

Module Example
    Public Sub Main()
        ' Create the token source.
        Dim cts As New CancellationTokenSource()

        ' Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
        Thread.Sleep(2500)

        ' Request cancellation by setting a flag on the token.
        cts.Cancel()
        Console.WriteLine("Cancellation set in token source...")
        Thread.Sleep(2500)
        ' Cancellation should have happened, so call Dispose.
        cts.Dispose()
    End Sub

    ' Thread 2: The listener
    Sub DoSomeWork(ByVal obj As Object)
        Dim token As CancellationToken = CType(obj, CancellationToken)

        For i As Integer = 0 To 1000000
            If token.IsCancellationRequested Then
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1)
                ' Perform cleanup if necessary.
                '...
                ' Terminate the operation.
                Exit For
            End If

            ' Simulate some work.
            Thread.SpinWait(500000)
        Next
    End Sub
End Module
' The example displays output like the following:
'       Cancellation set in token source...
'       In iteration 1430, cancellation has been requested...

Cancelamento de operação versus cancelamento de objeto

Na estrutura de cancelamento cooperativo, o cancelamento refere-se a operações, não a objetos. A solicitação de cancelamento significa que a operação deve ser interrompida o mais rápido possível após a conclusão de qualquer limpeza necessária. Um token de cancelamento deve se referir a uma "operação cancelável", no entanto, essa operação pode ser implementada em seu programa. Depois que a propriedade IsCancellationRequested do token foi configurada para true, não pode ser redefinida para false. Portanto, os tokens de cancelamento não podem ser reutilizados depois de terem sido cancelados.

Se você precisar de um mecanismo de cancelamento de objeto, pode baseá-lo no mecanismo de cancelamento da operação chamando o método CancellationToken.Register, como mostrado no exemplo a seguir.

using System;
using System.Threading;

class CancelableObject
{
    public string id;

    public CancelableObject(string id)
    {
        this.id = id;
    }

    public void Cancel()
    {
        Console.WriteLine("Object {0} Cancel callback", id);
        // Perform object cancellation here.
    }
}

public class Example1
{
    public static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        // User defined Class with its own method for cancellation
        var obj1 = new CancelableObject("1");
        var obj2 = new CancelableObject("2");
        var obj3 = new CancelableObject("3");

        // Register the object's cancel method with the token's
        // cancellation request.
        token.Register(() => obj1.Cancel());
        token.Register(() => obj2.Cancel());
        token.Register(() => obj3.Cancel());

        // Request cancellation on the token.
        cts.Cancel();
        // Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose();
    }
}
// The example displays the following output:
//       Object 3 Cancel callback
//       Object 2 Cancel callback
//       Object 1 Cancel callback
Imports System.Threading

Class CancelableObject
    Public id As String

    Public Sub New(id As String)
        Me.id = id
    End Sub

    Public Sub Cancel()
        Console.WriteLine("Object {0} Cancel callback", id)
        ' Perform object cancellation here.
    End Sub
End Class

Module Example
    Public Sub Main()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token

        ' User defined Class with its own method for cancellation
        Dim obj1 As New CancelableObject("1")
        Dim obj2 As New CancelableObject("2")
        Dim obj3 As New CancelableObject("3")

        ' Register the object's cancel method with the token's
        ' cancellation request.
        token.Register(Sub() obj1.Cancel())
        token.Register(Sub() obj2.Cancel())
        token.Register(Sub() obj3.Cancel())

        ' Request cancellation on the token.
        cts.Cancel()
        ' Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose()
    End Sub
End Module
' The example displays output like the following:
'       Object 3 Cancel callback
'       Object 2 Cancel callback
'       Object 1 Cancel callback

Se um objeto oferecer suporte a mais de uma operação simultânea cancelável, passe um token separado como entrada para cada operação cancelável distinta. Dessa forma, uma operação pode ser cancelada sem afetar as outras.

Detectar e responder a solicitações de cancelamento

No delegado do usuário, o implementador de uma operação cancelável determina como concluir a operação em resposta a uma solicitação de cancelamento. Em muitos casos, o delegado do usuário pode apenas executar qualquer limpeza necessária e depois retornar imediatamente.

No entanto, em casos mais complexos, pode ser necessário que o delegado do usuário notifique ao código da biblioteca que ocorreu o cancelamento. Nesses casos, a maneira correta de concluir a operação é o delegado chamar o método ThrowIfCancellationRequested, que fará com que um OperationCanceledException seja lançado. O código da biblioteca pode capturar essa exceção no thread do delegado de usuário e examinar o token da exceção para determinar se a exceção indica cancelamento cooperativo ou alguma outra situação excepcional.

A classe Task lida com o OperationCanceledException dessa forma. Para obter mais informações, consulte Cancelamento de tarefas.

Detectar por sondagem

Para cálculos de longa execução que fazem loop ou repetição, você pode detectar uma solicitação de cancelamento periodicamente pesquisando o valor da propriedade CancellationToken.IsCancellationRequested. Se o valor for true, o método deve ser limpo e concluído o mais rápido possível. A frequência de pesquisa ideal depende do tipo de aplicativo. Cabe ao desenvolvedor determinar a melhor frequência de sondagem para qualquer programa. A sondagem em si não afeta o desempenho significativamente. O exemplo a seguir mostra uma abordagem possível para a sondagem.

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
      // Assume that we know that the inner loop is very fast.
      // Therefore, polling once per column in the outer loop condition
      // is sufficient.
      for (int row = 0; row < rect.rows; row++) {
         // Simulating work.
         Thread.SpinWait(5_000);
         Console.Write("{0},{1} ", col, row);
      }
   }

   if (token.IsCancellationRequested) {
      // Cleanup or undo here if necessary...
      Console.WriteLine("\r\nOperation canceled");
      Console.WriteLine("Press any key to exit.");

      // If using Task:
      // token.ThrowIfCancellationRequested();
   }
}
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
    Dim col As Integer
    For col = 0 To rect.columns - 1
        ' Assume that we know that the inner loop is very fast.
        ' Therefore, polling once per column in the outer loop condition
        ' is sufficient.
        For col As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", x, y)
        Next
    Next

    If token.IsCancellationRequested = True Then
        ' Cleanup or undo here if necessary...
        Console.WriteLine(vbCrLf + "Operation canceled")
        Console.WriteLine("Press any key to exit.")

        ' If using Task:
        ' token.ThrowIfCancellationRequested()
    End If
End Sub

Para um exemplo mais completo, confira Como detectar solicitações de cancelamento por sondagem.

Ouvir ao registrar um retorno de chamada

Algumas operações podem ser bloqueadas de tal forma que não podem verificar o valor do token de cancelamento em tempo hábil. Para esses casos, você pode registrar um método de retorno de chamada que desbloqueia o método quando um pedido de cancelamento é recebido.

O método Register retorna um objeto CancellationTokenRegistration que é usado especificamente para essa finalidade. O exemplo a seguir mostra como usar o método Register para cancelar uma solicitação da Web assíncrona.

using System;
using System.Net;
using System.Threading;

class Example4
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        StartWebRequest(cts.Token);

        // cancellation will cause the web
        // request to be cancelled
        cts.Cancel();
    }

    static void StartWebRequest(CancellationToken token)
    {
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed.");

        // Cancellation on the token will
        // call CancelAsync on the WebClient.
        token.Register(() =>
        {
            wc.CancelAsync();
            Console.WriteLine("Request cancelled!");
        });

        Console.WriteLine("Starting request.");
        wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
    }
}
Imports System.Net
Imports System.Threading

Class Example
    Private Shared Sub Main()
        Dim cts As New CancellationTokenSource()

        StartWebRequest(cts.Token)

        ' cancellation will cause the web 
        ' request to be cancelled
        cts.Cancel()
    End Sub

    Private Shared Sub StartWebRequest(token As CancellationToken)
        Dim wc As New WebClient()
        wc.DownloadStringCompleted += Function(s, e) Console.WriteLine("Request completed.")

        ' Cancellation on the token will 
        ' call CancelAsync on the WebClient.
        token.Register(Function()
                           wc.CancelAsync()
                           Console.WriteLine("Request cancelled!")

                       End Function)

        Console.WriteLine("Starting request.")
        wc.DownloadStringAsync(New Uri("http://www.contoso.com"))
    End Sub
End Class

O objeto CancellationTokenRegistration gerencia a sincronização de thread e garante que o retorno de chamada será interrompido em um determinado momento.

Para garantir a capacidade de resposta do sistema e evitar deadlocks, as seguintes diretrizes devem ser seguidas ao registrar retornos de chamada:

  • O método de retorno de chamada deve ser rápido porque é chamado de forma síncrona e, portanto, a chamada para Cancel não retorna até o retorno da chamada.

  • Se você chamar o Dispose enquanto o retorno de chamada estiver sendo executado e segurar um bloqueio em que o retorno de chamada está aguardando, seu programa pode chegar a um deadlock. Depois do retorno de Dispose, você pode liberar todos os recursos necessários para o retorno de chamada.

  • Os retornos de chamada não devem executar qualquer thread manual ou uso de SynchronizationContext em um retorno de chamada. Se um retorno de chamada deve ser executado em um thread específico, use o construtor System.Threading.CancellationTokenRegistration que permite especificar que o destino syncContext é o SynchronizationContext.Current ativo. Realizar threading manual em um retorno de chamada pode causar um deadlock.

Para um exemplo mais completo, confira Como registrar retornos de chamada para solicitações de cancelamento.

Ouvir usando um identificador de espera

Quando uma operação cancelável pode bloquear enquanto aguarda um primitivo de sincronização, como System.Threading.ManualResetEvent ou System.Threading.Semaphore, você pode usar a propriedade CancellationToken.WaitHandle para permitir que a operação aguarde o evento e a solicitação de cancelamento. O identificador de espera do token de cancelamento será sinalizado em resposta a um pedido de cancelamento e o método pode usar o valor de retorno do método WaitAny para determinar se foi o token de cancelamento que sinalizou. A operação pode simplesmente sair ou acionar um OperationCanceledException, conforme apropriado.

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));
' Wait on the event if it is not signaled.
Dim waitHandles() As WaitHandle = {mre, token.WaitHandle}
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(waitHandles, _
                       New TimeSpan(0, 0, 20))

System.Threading.ManualResetEventSlim e System.Threading.SemaphoreSlim suportam a estrutura de cancelamento em seus métodos Wait. Você pode passar o CancellationToken para o método, e quando o cancelamento é solicitado, o evento é acionado e lança um OperationCanceledException.

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
Try
    ' mres is a ManualResetEventSlim
    mres.Wait(token)
Catch e As OperationCanceledException
    ' Throw immediately to be responsive. The
    ' alternative is to do one more item of work,
    ' and throw on next iteration, because
    ' IsCancellationRequested will be true.
    Console.WriteLine("Canceled while waiting.")
    Throw
End Try

' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)

Para um exemplo mais completo, confira Como detectar solicitações de cancelamento que possuem identificadores de espera.

Detectar vários tokens simultaneamente

Em alguns casos, um ouvinte pode ter que detectar vários tokens de cancelamento simultaneamente. Por exemplo, uma operação cancelável pode ter que monitorar um token de cancelamento interno, além de um token passado externamente como um argumento para um parâmetro de método. Para fazer isso, crie uma fonte de token vinculada que pode juntar dois ou mais tokens em um token, como mostrado no exemplo a seguir.

public void DoWork(CancellationToken externalToken)
{
    // Create a new token that combines the internal and external tokens.
    this.internalToken = internalTokenSource.Token;
    this.externalToken = externalToken;

    using (CancellationTokenSource linkedCts =
            CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
    {
        try
        {
            DoWorkInternal(linkedCts.Token);
        }
        catch (OperationCanceledException)
        {
            if (internalToken.IsCancellationRequested)
            {
                Console.WriteLine("Operation timed out.");
            }
            else if (externalToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancelling per user request.");
                externalToken.ThrowIfCancellationRequested();
            }
        }
    }
}
Public Sub DoWork(ByVal externalToken As CancellationToken)
    ' Create a new token that combines the internal and external tokens.
    Dim internalToken As CancellationToken = internalTokenSource.Token
    Dim linkedCts As CancellationTokenSource =
    CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
    Using (linkedCts)
        Try
            DoWorkInternal(linkedCts.Token)
        Catch e As OperationCanceledException
            If e.CancellationToken = internalToken Then
                Console.WriteLine("Operation timed out.")
            ElseIf e.CancellationToken = externalToken Then
                Console.WriteLine("Canceled by external token.")
                externalToken.ThrowIfCancellationRequested()
            End If
        End Try
    End Using
End Sub

Observe que você deve chamar Dispose na fonte de token vinculada quando concluir. Para um exemplo mais completo, confira Como detectar múltiplas solicitações de cancelamento.

Cooperação entre o código de biblioteca e o código do usuário

A estrutura de cancelamento unificada permite que o código da biblioteca cancele o código do usuário e que o código do usuário cancele o código da biblioteca de forma cooperativa. A cooperação sem problemas depende da conformidade com as seguintes diretrizes:

  • Se o código da biblioteca fornece operações canceláveis, ele também deve fornecer métodos públicos que aceitam um token de cancelamento externo para que o código do usuário possa solicitar o cancelamento.

  • Se o código da biblioteca chamar o código do usuário, o código da biblioteca deve interpretar um OperationCanceledException(externalToken) como cancelamento cooperativo e não necessariamente como uma exceção de falha.

  • Os delegados de usuários devem tentar responder as solicitações de cancelamento do código da biblioteca em tempo hábil.

System.Threading.Tasks.Task e System.Linq.ParallelEnumerable são exemplos de classes que seguem essas diretrizes. Para obter mais informações, consulte Cancelamento de tarefase Como cancelar uma consulta PLINQ.

Confira também