Partilhar 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 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 threads ou tarefas, passa o token para cada operação. Operações individuais podem, por sua vez, 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 parem o que estão fazendo. Somente o objeto solicitante pode emitir o pedido de cancelamento, e cada ouvinte é responsável por perceber a solicitação e respondê-la de forma adequada e oportuna.

O padrão geral para a implementação do modelo de cancelamento cooperativo é:

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

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

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

  • Ligue para o método CancellationTokenSource.Cancel para fornecer 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 do token de cancelamento para liberar quaisquer recursos não gerenciados que ele contenha.

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 aplicações e bibliotecas conscientes do cancelamento e suporta os seguintes recursos:

  • O cancelamento é cooperativo e não é imposto ao ouvinte. O ouvinte determina como encerrar graciosamente em resposta a uma solicitação de cancelamento.

  • Pedir é diferente de ouvir. Um objeto que invoca uma operação cancelável pode controlar quando (se nunca) 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, unindo-os num token vinculado.

  • O código de usuário pode notificar e responder a solicitações de cancelamento do código da biblioteca, e o código da biblioteca pode notificar e responder a solicitações de cancelamento do código do usuário.

  • Os ouvintes podem ser notificados de solicitações de cancelamento por sondagem, registro de retorno de chamada ou aguardando em alças 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 monitorizam o valor da propriedade IsCancellationRequested do token através de sondagem, callback ou identificador de espera.
OperationCanceledException As sobrecargas do construtor desta 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.Task, System.Threading.Tasks.Task<TResult> e System.Linq.ParallelEnumerable. Recomendamos que você use este 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 monitoriza o valor da propriedade IsCancellationRequested do token através de sondagem. Quando o valor se torna true, o ouvinte pode encerrar da maneira que for apropriada. Neste exemplo, o método simplesmente termina, que é tudo o que é 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 APIs herdadas. Para obter um exemplo que utiliza o tipo preferido System.Threading.Tasks.Task, consulte Como cancelar uma tarefa e suas subtarefas.

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 da operação versus cancelamento do objeto

No quadro de cancelamento cooperativo, o cancelamento refere-se a operações e não a objetos. O pedido de cancelamento significa que a operação deve parar o mais rapidamente possível após a realização de qualquer limpeza necessária. Um token de cancelamento deve referir-se a uma "operação cancelável", no entanto, essa operação pode ser implementada no seu programa. Depois que a propriedade IsCancellationRequested 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 depois de terem sido cancelados.

Se você precisar de um mecanismo de cancelamento de objeto, poderá baseá-lo no mecanismo de cancelamento de operação chamando o método CancellationToken.Register, 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 suportar 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.

Ouvir e Responder a Pedidos de Cancelamento

No delegado de 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 apenas executar qualquer limpeza necessária e, em seguida, retornar imediatamente.

No entanto, em casos mais complexos, pode ser necessário que o delegado do usuário notifique o código da biblioteca de que o cancelamento ocorreu. Nesses casos, a maneira correta de encerrar a operação é o delegado invocar o método ThrowIfCancellationRequested, o que resultará no lançamento de uma OperationCanceledException. O código da biblioteca pode interceptar esta exceção na thread de delegação do utilizador e analisar o token da exceção para determinar se a exceção indica um cancelamento cooperativo ou alguma outra situação excecional.

A classe Task lida com OperationCanceledException desta forma. Para obter mais informações, consulte Cancelamento de Tarefas.

Ouvir por sondagem

Para cálculos de longa duração que fazem loop ou repetência, você pode ouvir uma solicitação de cancelamento pesquisando periodicamente o valor da propriedade CancellationToken.IsCancellationRequested. Se o seu valor for true, o método deve limpar e terminar o mais rapidamente possível. A frequência ideal de sondagem depende do tipo de aplicação. Cabe ao desenvolvedor determinar a melhor frequência de sondagem para qualquer programa. A sondagem em si não afeta significativamente o desempenho. O exemplo a seguir mostra uma maneira possível de 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 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: Ouvir solicitações de cancelamento por sondagem.

Ouvindo através do registo de uma chamada de retorno

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, pode-se registar um método de retorno de chamada que desbloqueia o método quando uma solicitação de cancelamento é recebida.

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.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 objeto CancellationTokenRegistration gerencia a sincronização de encadeamentos e garante que o callback deixe de ser executado em um ponto exato no tempo.

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

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

  • Se chamar Dispose enquanto o retorno de chamada está a decorrer e estiver a manter um bloqueio que o retorno de chamada está a aguardar, o seu programa pode ficar num impasse. Após o retorno de Dispose, pode-se liberar quaisquer recursos exigidos pelo retorno de chamada.

  • Os retornos de chamada não devem executar nenhum uso manual de threads ou uso de SynchronizationContext dentro de um retorno de chamada. Se um callback deve ser executado em um thread específico, use o construtor System.Threading.CancellationTokenRegistration que permite definir que o syncContext de destino é o SynchronizationContext.Currentativo. Executar encadeamento manual num callback pode causar interbloqueio.

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

Escutando usando um mecanismo de espera

Quando uma operação cancelável pode ser bloqueada enquanto aguarda por uma primitiva de sincronização, como um System.Threading.ManualResetEvent ou System.Threading.Semaphore, você pode usar a propriedade CancellationToken.WaitHandle para que a operação possa aguardar tanto o evento quanto a solicitação de cancelamento. O identificador de espera do token de cancelamento será sinalizado em resposta a uma solicitação 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 então simplesmente terminar ou lançar 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 apoiam 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 acorda e gera 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 obter um exemplo mais completo, consulte Como: Ouvir solicitações de cancelamento que têm alças de espera.

Ouvindo vários tokens simultaneamente

Em alguns casos, um ouvinte pode ter que ouvir 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 terminar de usá-la. Para obter um exemplo mais completo, consulte Como ouvir várias solicitações de cancelamento.

Cooperação entre o Código da Biblioteca e o Código do Utilizador

A estrutura de cancelamento unificada possibilita 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 boa cooperação depende de cada parte seguir estas diretrizes:

  • Se o código da biblioteca fornecer operações canceláveis, ele também deverá fornecer métodos públicos que aceitem 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 deverá interpretar uma OperationCanceledException(externalToken) como um cancelamento cooperativo e não necessariamente como uma exceção de falha.

  • Os representantes de usuários 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 Tarefas e Como cancelar uma consulta PLINQ.

Ver também