Quando descartar o CancellationTokenSource?

163

A classe CancellationTokenSourceé descartável. Uma rápida olhada no Reflector comprova o uso de KernelEvent, um recurso (provavelmente) não gerenciado. Como CancellationTokenSourcenão possui finalizador, se não descartá-lo, o GC não o fará.

Por outro lado, se você observar os exemplos listados no artigo do MSDN Cancelamento em threads gerenciados , apenas um trecho de código descartará o token.

Qual é a maneira correta de descartá-lo em código?

  1. Você não pode quebrar o código iniciando sua tarefa paralela usingse não o esperar. E faz sentido ter cancelamento apenas se você não esperar.
  2. Claro que você pode adicionar ContinueWithtarefas com uma Disposeligação, mas é esse o caminho a seguir?
  3. E as consultas canceláveis ​​do PLINQ, que não são sincronizadas novamente, mas apenas fazem algo no final? Vamos dizer .ForAll(x => Console.Write(x))?
  4. É reutilizável? O mesmo token pode ser usado para várias chamadas e depois descartá-lo com o componente host, digamos, controle da interface do usuário?

Como ele não possui algo como um Resetmétodo para limpeza IsCancelRequestede Tokencampo, eu suponho que não seja reutilizável; portanto, toda vez que você inicia uma tarefa (ou uma consulta PLINQ), deve criar uma nova. É verdade? Se sim, minha pergunta é qual é a estratégia correta e recomendada para lidar com Disposeessas muitas CancellationTokenSourceinstâncias?

George Mamaladze
fonte

Respostas:

81

Falando sobre se é realmente necessário ligar para Dispose CancellationTokenSource... Eu tive um vazamento de memória no meu projeto e acabou que CancellationTokenSourceera o problema.

Meu projeto tem um serviço, que lê constantemente o banco de dados e dispara tarefas diferentes, e eu estava passando tokens de cancelamento vinculados aos meus funcionários; portanto, mesmo depois de terem terminado o processamento dos dados, os tokens de cancelamento não eram descartados, o que causava um vazamento de memória.

O cancelamento do MSDN nos segmentos gerenciados afirma claramente:

Observe que você deve chamar Disposea fonte de token vinculada quando terminar. Para um exemplo mais completo, consulte Como: ouvir várias solicitações de cancelamento .

Eu usei ContinueWithna minha implementação.

Gruzilkin
fonte
14
Essa é uma omissão importante na resposta atual aceita por Bryan Crosby - se você criar um CTS vinculado , corre o risco de vazar memória. O cenário é muito semelhante aos manipuladores de eventos que nunca são registrados.
Søren Boisen
5
Eu tive um vazamento devido a esse mesmo problema. Usando um criador de perfil, pude ver registros de retorno de chamada com referências às instâncias CTS vinculadas. Examinar o código para a implementação do CTS Dispose aqui foi muito esclarecedor e enfatiza a comparação do @ SørenBoisen com os vazamentos de registro do manipulador de eventos.
BitMask777 03/03
Os comentários acima refletem o estado da discussão e a outra resposta de @Bryan Crosby foi aceita.
George Mamaladze 7/03
A documentação em 2020 diz claramente: Important: The CancellationTokenSource class implements the IDisposable interface. You should be sure to call the CancellationTokenSource.Dispose method when you have finished using the cancellation token source to free any unmanaged resources it holds.- docs.microsoft.com/en-us/dotnet/standard/threading/…
Endrju
44

Não achei que nenhuma das respostas atuais fosse satisfatória. Depois de pesquisar, encontrei esta resposta de Stephen Toub ( referência ):

Depende. No .NET 4, o CTS.Dispose atendia a dois propósitos principais. Se o WaitHandle do CancellationToken tiver sido acessado (alocando-o preguiçosamente), o Dispose descartará esse identificador. Além disso, se o CTS foi criado pelo método CreateLinkedTokenSource, Dispose desvinculará o CTS dos tokens aos quais estava vinculado. No .NET 4.5, Dispose tem uma finalidade adicional, ou seja, se o CTS usar um Timer debaixo das cobertas (por exemplo, CancelAfter foi chamado), o Timer será Disposed.

É muito raro o CancellationToken.WaitHandle ser usado, portanto, limpar depois dele normalmente não é um ótimo motivo para usar o Dispose. Se, no entanto, você estiver criando seu CTS com CreateLinkedTokenSource, ou se estiver usando a funcionalidade de timer do CTS, poderá ser mais impactante usar Dispose.

A parte ousada que eu acho que é a parte importante. Ele usa "mais impactante", o que deixa um pouco vago. Estou interpretando como significando que a chamada Disposenessas situações deve ser feita, caso contrário, o uso Disposenão é necessário.

Jesse Good
fonte
10
Mais impactante significa que o CTS filho é adicionado ao pai. Se você não descartar a criança, haverá um vazamento se os pais tiverem vida longa. Portanto, é fundamental descartar os vinculados.
Grigory
26

Dei uma olhada no ILSpy, CancellationTokenSourcemas só consigo descobrir m_KernelEventqual é realmente um ManualResetEvent, que é uma classe de wrapper para um WaitHandleobjeto. Isso deve ser tratado adequadamente pelo GC.

Bryan Crosby
fonte
7
Tenho a mesma sensação de que a GC vai limpar tudo isso. Vou tentar verificar isso. Por que a Microsoft implementou o descarte nesse caso? Para se livrar dos retornos de chamada de eventos e evitar a propagação para o GC de segunda geração, provavelmente. Nesse caso, chamar Dispose é opcional - chame-o se puder, se não apenas ignore. Não é a melhor maneira que eu penso.
George Mamaladze 5/08
4
Eu investiguei esse problema. CancellationTokenSource obtém o lixo coletado. Você pode ajudar com o descarte no GEN 1 GC. Aceitaram.
George Mamaladze 9/08
1
Eu fiz a mesma investigação de forma independente e cheguei à mesma conclusão: descarte se puder facilmente, mas não se preocupe em tentar fazê-lo nos casos raros, mas não inéditos, em que você enviou um CancelamentoToken para os boondock e não querem esperar que eles escrevam um cartão postal dizendo que já terminaram. Isso vai acontecer de vez em quando devido à natureza do uso do CancellationToken, e está tudo bem, prometo.
Joe Amenta
6
Meu comentário acima não se aplica a fontes de token vinculadas; Não pude provar que não há problema em deixá-las indisponíveis, e a sabedoria nesse segmento e no MSDN sugere que talvez não.
9788 Joe Amenta
23

Você deve sempre descartar CancellationTokenSource.

Como descartá-lo depende exatamente do cenário. Você propõe vários cenários diferentes.

  1. usingsó funciona quando você está usando CancellationTokenSourcealgum trabalho paralelo que está esperando. Se esse é o seu senario, então ótimo, é o método mais fácil.

  2. Ao usar tarefas, use uma ContinueWithtarefa conforme indicado para descartar CancellationTokenSource.

  3. Para o plinq, você pode usá- usinglo desde que o esteja executando em paralelo, mas aguardando a conclusão de todos os trabalhadores em execução paralelo.

  4. Para a interface do usuário, você pode criar um novo CancellationTokenSourcepara cada operação cancelável que não esteja vinculada a um único gatilho de cancelamento. Mantenha ae List<IDisposable>adicione cada fonte à lista, descartando todas quando o componente for descartado.

  5. Para threads, crie um novo thread que junte todos os threads de trabalho e feche a fonte única quando todos os threads de trabalho forem concluídos. Consulte CancellationTokenSource, quando descartar?

Sempre tem um jeito. IDisposableinstâncias devem sempre ser descartadas. As amostras geralmente não o fazem porque são amostras rápidas para mostrar o uso principal ou porque adicionar todos os aspectos da classe que está sendo demonstrada seria muito complexo para uma amostra. A amostra é apenas uma amostra, não necessariamente (ou mesmo geralmente) código de qualidade da produção. Nem todas as amostras são aceitáveis ​​para serem copiadas no código de produção como estão.

Samuel Neff
fonte
para o ponto 2, algum motivo para você não poder usar awaita tarefa e dispor o CancellationTokenSource no código que vem depois da espera?
stijn
14
Existem advertências. Se o CTS for cancelado durante awaituma operação, você poderá retomar devido a um OperationCanceledException. Você pode então ligar Dispose(). Porém, se ainda houver operações em execução e usando o correspondente CancellationToken, esse token ainda será relatado CanBeCanceledcomo sendo truea fonte descartada. Se eles tentarem registrar um retorno de chamada de cancelamento, BOOM! , ObjectDisposedException. É suficientemente seguro ligar Dispose()após a conclusão bem-sucedida da (s) operação (ões). Fica realmente complicado quando você realmente precisa cancelar algo.
Mike Strobel
8
Votado pelas razões apontadas por Mike Strobel - forçar uma regra a sempre chamar Dispose pode levar você a situações difíceis ao lidar com o CTS e o Task devido à sua natureza assíncrona. A regra deve ser: descarte sempre as fontes de token vinculadas .
Søren Boisen
1
O seu link vai para uma resposta excluída.
Trisped
19

Essa resposta ainda está aparecendo nas pesquisas do Google e acredito que a resposta votada não fornece a história completa. Depois de olhar sobre o código-fonte para CancellationTokenSource(CTS) e CancellationToken(CT) Eu acredito que para a maioria dos casos de uso a seqüência de código a seguir é bom:

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}

O m_kernelHandlecampo interno mencionado acima é o objeto de sincronização que suporta a WaitHandlepropriedade nas classes CTS e CT. É instanciado apenas se você acessar essa propriedade. Portanto, a menos que você esteja usando WaitHandlealguma sincronização de threads da velha escola em seu Taskdescarte de chamadas, não terá efeito.

Obviamente, se você o estiver usando, faça o que é sugerido pelas outras respostas acima e adie a chamada Disposeaté que todas as WaitHandleoperações usando o identificador sejam concluídas, porque, conforme descrito na documentação da API do Windows para WaitHandle , os resultados são indefinidos.

jlyonsmith
fonte
7
O artigo Cancelamento do MSDN nos segmentos gerenciados declara: "Os ouvintes monitoram o valor da IsCancellationRequestedpropriedade do token pesquisando, retornando a chamada ou identificando a espera". Em outras palavras: Pode não ser você (por exemplo, quem está fazendo a solicitação assíncrona) que usa a alça de espera, pode ser o ouvinte (por exemplo, aquele que responde à solicitação). O que significa que você, como responsável pela eliminação, efetivamente não tem controle sobre o uso ou não da alça de espera.
precisa saber é o seguinte
De acordo com o MSDN, retornos de chamada registrados com exceção farão com que .Cancel seja lançado. Seu código não chamará .Dispose () se isso acontecer. Os retornos de chamada devem ter cuidado para não fazer isso, mas isso pode acontecer.
Joseph Lennox
11

Já faz muito tempo que eu perguntei isso e obtive muitas respostas úteis, mas me deparei com uma questão interessante relacionada a isso e pensei em publicá-la aqui como outra resposta:

Você deve ligar CancellationTokenSource.Dispose()apenas quando tiver certeza de que ninguém tentará obter as Tokenpropriedades do CTS . Caso contrário, você não deve chamá-lo, porque é uma corrida. Por exemplo, veja aqui:

https://github.com/aspnet/AspNetKatana/issues/108

Na correção desse problema, o código que cts.Cancel(); cts.Dispose();foi feito anteriormente foi editado para ser feito, cts.Cancel();porque qualquer um que tenha azar de tentar obter o token de cancelamento para observar seu estado de cancelamento após a Dispose chamada, infelizmente também precisará lidar ObjectDisposedException- além do OperationCanceledExceptionque eles estavam planejando.

Outra observação importante relacionada a essa correção é feita pelo Tratcher: "O descarte é necessário apenas para tokens que não serão cancelados, pois o cancelamento faz a mesma limpeza". ou seja, apenas fazer em Cancel()vez de descartar é realmente bom o suficiente!

Tim Lovell-Smith
fonte
1

Eu criei uma classe thread-safe que vincula a CancellationTokenSourcea Taske garante que CancellationTokenSourceele será descartado quando o associado for Taskconcluído. Ele usa bloqueios para garantir que o CancellationTokenSourcenão seja cancelado durante ou após o descarte. Isso ocorre para conformidade com a documentação , que afirma:

O Disposemétodo deve ser usado apenas quando todas as outras operações no CancellationTokenSourceobjeto forem concluídas.

E também :

O Disposemétodo deixa o CancellationTokenSourceem um estado inutilizável.

Aqui está a classe:

public class CancelableExecution
{
    private readonly bool _allowConcurrency;
    private Operation _activeOperation;

    private class Operation : IDisposable
    {
        private readonly object _locker = new object();
        private readonly CancellationTokenSource _cts;
        private readonly TaskCompletionSource<bool> _completionSource;
        private bool _disposed;

        public Task Completion => _completionSource.Task; // Never fails

        public Operation(CancellationTokenSource cts)
        {
            _cts = cts;
            _completionSource = new TaskCompletionSource<bool>(
                TaskCreationOptions.RunContinuationsAsynchronously);
        }
        public void Cancel()
        {
            lock (_locker) if (!_disposed) _cts.Cancel();
        }
        void IDisposable.Dispose() // Is called only once
        {
            try
            {
                lock (_locker) { _cts.Dispose(); _disposed = true; }
            }
            finally { _completionSource.SetResult(true); }
        }
    }

    public CancelableExecution(bool allowConcurrency)
    {
        _allowConcurrency = allowConcurrency;
    }
    public CancelableExecution() : this(false) { }

    public bool IsRunning =>
        Interlocked.CompareExchange(ref _activeOperation, null, null) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> taskFactory,
        CancellationToken extraToken = default)
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken, default);
        using (var operation = new Operation(cts))
        {
            // Set this as the active operation
            var oldOperation = Interlocked.Exchange(ref _activeOperation, operation);
            try
            {
                if (oldOperation != null && !_allowConcurrency)
                {
                    oldOperation.Cancel();
                    await oldOperation.Completion; // Continue on captured context
                }
                var task = taskFactory(cts.Token); // Run in the initial context
                return await task.ConfigureAwait(false);
            }
            finally
            {
                // If this is still the active operation, set it back to null
                Interlocked.CompareExchange(ref _activeOperation, null, operation);
            }
        }
    }

    public Task RunAsync(Func<CancellationToken, Task> taskFactory,
        CancellationToken extraToken = default)
    {
        return RunAsync<object>(async ct =>
        {
            await taskFactory(ct).ConfigureAwait(false);
            return null;
        }, extraToken);
    }

    public Task CancelAsync()
    {
        var operation = Interlocked.CompareExchange(ref _activeOperation, null, null);
        if (operation == null) return Task.CompletedTask;
        operation.Cancel();
        return operation.Completion;
    }

    public bool Cancel() => CancelAsync() != Task.CompletedTask;
}

Os principais métodos da CancelableExecutionclasse são the RunAsynce the Cancel. Por padrão, operações simultâneas não são permitidas, o que significa que RunAsynca segunda chamada cancelará silenciosamente e aguardará a conclusão da operação anterior (se ainda estiver em execução), antes de iniciar a nova operação.

Essa classe pode ser usada em aplicativos de qualquer tipo. Seu uso principal, porém, é em aplicativos de interface do usuário, dentro de formulários com botões para iniciar e cancelar uma operação assíncrona ou com uma caixa de listagem que cancela e reinicia uma operação toda vez que seu item selecionado é alterado. Aqui está um exemplo do primeiro caso:

private readonly CancelableExecution _cancelableExecution = new CancelableExecution();

private async void btnExecute_Click(object sender, EventArgs e)
{
    string result;
    try
    {
        Cursor = Cursors.WaitCursor;
        btnExecute.Enabled = false;
        btnCancel.Enabled = true;
        result = await _cancelableExecution.RunAsync(async ct =>
        {
            await Task.Delay(3000, ct); // Simulate some cancelable I/O operation
            return "Hello!";
        });
    }
    catch (OperationCanceledException)
    {
        return;
    }
    finally
    {
        btnExecute.Enabled = true;
        btnCancel.Enabled = false;
        Cursor = Cursors.Default;
    }
    this.Text += result;
}

private void btnCancel_Click(object sender, EventArgs e)
{
    _cancelableExecution.Cancel();
}

O RunAsyncmétodo aceita um extra CancellationTokencomo argumento, vinculado ao criado internamente CancellationTokenSource. O fornecimento desse token opcional pode ser útil em cenários avançados.

Theodor Zoulias
fonte