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 CancellationTokenSource objeto, que gerencia e envia notificação de cancelamento para os tokens de cancelamento individuais.

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

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

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

Importante

A classe CancellationTokenSource implementa a interface IDisposable. Certifique-se de chamar o CancellationTokenSource.Dispose método quando terminar de usar a fonte do token de cancelamento para liberar quaisquer recursos não gerenciados que ele detém.

A ilustração a seguir mostra a relação 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 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 em um 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 Description
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 do token por sondagem, retorno de chamada ou identificador de IsCancellationRequested espera.
OperationCanceledException Sobrecargas do construtor desta exceção aceitam a CancellationToken como um 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 CancellationTokenSource objeto e, em seguida, passa sua Token propriedade para a operação cancelável. A operação que recebe a solicitação monitora o IsCancellationRequested valor da propriedade do token por sondagem. Quando o valor se torna true, o ouvinte pode terminar da maneira que for apropriada. Neste exemplo, o método simplesmente sai, que é tudo o que é necessário em muitos casos.

Nota

O exemplo usa o QueueUserWorkItem método para demonstrar que a estrutura de cancelamento cooperativo é compatível com APIs herdadas. Para obter 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 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 IsCancellationRequested propriedade do token tiver sido definida como true, ele não poderá ser redefinido 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 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 {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 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 é que o delegado chame o método , o ThrowIfCancellationRequestedque fará com que um OperationCanceledException seja lançado. O código da biblioteca pode capturar essa exceção no thread de 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 excecional.

A Task classe lida 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 recurse, você pode ouvir uma solicitação de cancelamento pesquisando periodicamente o valor da CancellationToken.IsCancellationRequested propriedade. 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 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 obter um exemplo mais completo, consulte Como: Ouvir solicitações de cancelamento por sondagem.

Escutando registrando 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 uma solicitação de cancelamento é recebida.

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

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 CancellationTokenRegistration objeto gerencia a sincronização de threads e garante que o retorno de chamada pare de ser executado em um ponto preciso 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 porque é chamado de forma síncrona e, portanto, a chamada para Cancel não retorna até que o retorno de chamada retorne.

  • Se você ligar Dispose enquanto o retorno de chamada está em execução e você mantém um bloqueio que o retorno de chamada está aguardando, seu programa pode bloquear. Após Dispose os retornos, você pode liberar todos os recursos exigidos pelo retorno de chamada.

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

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

Escutando usando uma alça de espera

Quando uma operação cancelável pode ser bloqueada enquanto aguarda em uma primitiva de sincronização, como a System.Threading.ManualResetEvent ou System.Threading.Semaphore, você pode usar a CancellationToken.WaitHandle propriedade 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 uma solicitação de cancelamento, e o método pode usar o valor de retorno do método para determinar se foi o token de WaitAny cancelamento que sinalizou. A operação pode então simplesmente sair, 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 ambos apoiam o quadro de cancelamento nos seus Wait métodos. Você pode passar o CancellationToken para o método, e quando o cancelamento é solicitado, o evento acorda 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 obter um exemplo mais completo, consulte Como ouvir solicitações de cancelamento com 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 a 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 um OperationCanceledException(externalToken) como 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.

Consulte também