Compartilhar via


Cancelamento em threads gerenciados

A partir do .NET Framework 4, o .NET usa um modelo unificado para cancelamento cooperativo de operações síncronas assíncronas ou de execução longa. Esse 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 threads ou tarefas, passa o token para cada operação. As operações individuais, por sua vez, podem passar cópias do token para outras operações. Em algum momento posterior, o objeto que criou o token pode usá-lo para solicitar que as operações interrompam 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 maneira apropriada e oportuna.

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

  • Instancie um CancellationTokenSource objeto, 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 CancellationTokenSource.Cancel método para fornecer notificação de cancelamento.

Importante

A classe CancellationTokenSource implementa a interface IDisposable. Você deve ter certeza de chamar o método CancellationTokenSource.Dispose quando terminar de usar a fonte do token de cancelamento, a fim de liberar todos os recursos não gerenciados que ela contém.

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

CancellationTokenSource e tokens de cancelamento

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

  • O cancelamento é cooperativo e não é forçado no ouvinte. O ouvinte determina como terminar de forma graciosa em resposta a uma solicitação de cancelamento.

  • A solicitação é distinta da escuta. Um objeto que invoca uma operação cancelável pode controlar quando (se alguma vez) o cancelamento é solicitado.

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

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

  • O código do usuário pode observar e responder a solicitações de cancelamento do código da biblioteca, e o código da biblioteca pode observar e responder a 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 são listados na tabela a seguir.

Nome do tipo Descrição
CancellationTokenSource Objeto que cria um token de cancelamento e também emite a solicitação de cancelamento para todas as cópias desse token.
CancellationToken Tipo de valor leve passado para 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 Sobrecargas do construtor dessa exceção aceitam um CancellationToken como parâmetro. Opcionalmente, os ouvintes podem lançar essa exceção para verificar a origem do cancelamento e notificar outras pessoas de que ele respondeu a uma solicitação de cancelamento.

O modelo de cancelamento é integrado ao .NET em vários tipos. Os mais importantes são System.Threading.Tasks.Parallel, System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> e System.Linq.ParallelEnumerable. Recomendamos que você use esse modelo de cancelamento cooperativo para todas as novas bibliotecas e códigos de aplicativo.

Exemplo de código

No exemplo a seguir, o objeto solicitante cria um CancellationTokenSource objeto e passa sua Token propriedade 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 terminar de qualquer maneira apropriada. Neste exemplo, o método apenas sai, que é tudo necessário em muitos casos.

Observação

O exemplo usa o QueueUserWorkItem método para demonstrar que a estrutura de cancelamento cooperativa é compatível com 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 Example1
    Public Sub Main1()
        ' 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 parar o mais rápido possível depois que qualquer limpeza necessária for executada. 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 IsCancellationRequested propriedade do token tiver sido definida como true, ela não poderá ser redefinida para false. Portanto, os tokens de cancelamento não podem ser reutilizados após terem sido cancelados.

Se você precisar de um mecanismo de cancelamento de objeto, poderá baseá-lo no mecanismo de cancelamento da operação chamando o CancellationToken.Register método, conforme 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 {id} Cancel callback");
        // 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 ExampleOb1
    Public Sub MainOb1()
        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 der suporte a mais de uma operação cancelável simultânea, 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.

Escutando e respondendo a solicitações de cancelamento

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

No entanto, em casos mais complexos, pode ser necessário que o representante do usuário notifique o código da biblioteca de que o cancelamento ocorreu. 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 delegado do 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 Task classe manipula OperationCanceledException dessa maneira. Para obter mais informações, consulte Cancelamento de Tarefa.

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 deverá limpar e terminar o mais rápido possível. A frequência ideal de sondagem depende do tipo de aplicativo. Cabe ao desenvolvedor determinar a melhor frequência de sondagem para qualquer programa específico. A sondagem em si não afeta significativamente o desempenho. O exemplo a seguir mostra uma maneira possível de sondar.

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 row As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", col, row)
        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 obter um exemplo mais completo, consulte Como escutar solicitações de cancelamento por sondagem.

Ouvir ao registrar um retorno de chamada

Algumas operações podem ser bloqueadas de forma que não possam 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 Register método retorna um CancellationTokenRegistration objeto que é usado especificamente para essa finalidade. O exemplo a seguir mostra como usar o Register método para cancelar uma solicitação da Web assíncrona.

using System;
using System.Net.Http;
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)
    {
        var client = new HttpClient();

        token.Register(() =>
        {
            client.CancelPendingRequests();
            Console.WriteLine("Request cancelled!");
        });

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

Class Example4
    Private Shared Sub Main4()
        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 client As New HttpClient()

        token.Register(Sub()
                           client.CancelPendingRequests()
                           Console.WriteLine("Request cancelled!")
                       End Sub)

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

O CancellationTokenRegistration objeto gerencia a sincronização de threads e garante que o callback pare de executar em um ponto preciso no tempo.

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 obter um exemplo mais completo, consulte 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 ambos dão suporte à estrutura de cancelamento em seus Wait métodos. 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 possa unir dois ou mais tokens em um token, conforme 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 obter um exemplo mais completo, consulte Como escutar várias solicitações de cancelamento.

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

A estrutura de cancelamento unificada possibilita que o código de biblioteca cancele o código do usuário e que o código do usuário cancele o código da biblioteca de maneira cooperativa. A cooperação harmoniosa depende de que cada lado siga estas diretrizes.

  • Se o código da biblioteca fornecer operações canceláveis, ele também deverá fornecer métodos públicos que aceitam um token de cancelamento externo para que o código do usuário possa solicitar 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 representantes do usuário devem tentar responder às 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 Tarefa e Como cancelar uma consulta PLINQ.

Consulte também