Quando smaltire CancelTokenSource?


163

La classe CancellationTokenSourceè usa e getta. Una rapida occhiata in Reflector dimostra l'utilizzo di KernelEventuna (molto probabilmente) risorsa non gestita. Poiché CancellationTokenSourcenon ha un finalizzatore, se non lo eliminiamo, il GC non lo farà.

D'altra parte, se si esaminano gli esempi elencati nell'articolo MSDN Cancellazione nei thread gestiti , solo un frammento di codice dispone del token.

Qual è il modo corretto di smaltirlo nel codice?

  1. Non è possibile racchiudere il codice avviando l'attività parallela con usingse non la si attende. E ha senso avere la cancellazione solo se non aspetti.
  2. Naturalmente puoi aggiungere un ContinueWithcompito con una Disposechiamata, ma è quella la strada da percorrere?
  3. Che dire delle query PLINQ annullabili, che non si sincronizzano di nuovo, ma alla fine fanno qualcosa? Diciamo .ForAll(x => Console.Write(x))?
  4. È riutilizzabile? Lo stesso token può essere utilizzato per diverse chiamate e quindi eliminarlo insieme al componente host, diciamo controllo dell'interfaccia utente?

Perché non ha qualcosa come un Resetmetodo per clean-up IsCancelRequestede Tokencampo mi immagino sia non riutilizzabile, quindi ogni volta che si avvia un'attività (o di una query PLINQ) si dovrebbe creare una nuova. È vero? In caso affermativo, la mia domanda è: qual è la strategia corretta e consigliata da affrontare Disposein questi molti CancellationTokenSourcecasi?

Risposte:


82

Parlando se è davvero necessario chiamare Dispose su CancellationTokenSource... Ho avuto una perdita di memoria nel mio progetto e si è scoperto che CancellationTokenSourceera il problema.

Il mio progetto ha un servizio, che legge costantemente il database e avvia diverse attività, e stavo trasmettendo token di annullamento collegati ai miei dipendenti, quindi anche dopo aver terminato l'elaborazione dei dati, i token di annullamento non sono stati eliminati, causando una perdita di memoria.

La cancellazione MSDN nei thread gestiti indica chiaramente:

Si noti che è necessario chiamare Disposel'origine token collegata al termine dell'operazione. Per un esempio più completo, vedere Procedura: ascoltare richieste di annullamento multiple .

Ho usato ContinueWithnella mia implementazione.


14
Questa è un'omissione importante nell'attuale risposta accettata da Bryan Crosby: se si crea un CTS collegato , si rischiano perdite di memoria. Lo scenario è molto simile ai gestori di eventi che non vengono mai registrati.
Søren Boisen,

5
Ho avuto una perdita a causa di questo stesso problema. Utilizzando un profiler ho potuto vedere le registrazioni di callback che contenevano riferimenti alle istanze CTS collegate. Esaminare il codice per l'implementazione di CTS Dispose qui è stato molto approfondito, e sottolinea il confronto di @ SørenBoisen con le perdite di registrazione del gestore di eventi.
BitMask777

I commenti sopra riflettono lo stato della discussione in cui è stata accettata l'altra risposta di @Bryan Crosby.
George Mamaladze,

La documentazione del 2020 dice chiaramente: 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

Non pensavo che nessuna delle risposte attuali fosse soddisfacente. Dopo le ricerche ho trovato questa risposta di Stephen Toub ( riferimento ):

Dipende. In .NET 4, CTS.Dispose ha avuto due scopi principali. Se fosse stato effettuato l'accesso a WaitHandle di CancelToken (allocandolo pigramente), Dispose eliminerà tale handle. Inoltre, se il CTS è stato creato tramite il metodo CreateLinkedTokenSource, Dispose scollegherà il CTS dai token a cui era collegato. In .NET 4.5, Dispose ha uno scopo aggiuntivo, vale a dire se il CTS utilizza un timer sotto le copertine (ad es. CancelAfter è stato chiamato), il timer verrà eliminato.

È molto raro che CancelToken.WaitHandle venga utilizzato, quindi la pulizia dopo non è in genere un ottimo motivo per utilizzare Dispose. Se, tuttavia, stai creando il tuo CTS con CreateLinkedTokenSource o se stai utilizzando la funzionalità timer del CTS, può essere più efficace utilizzare Dispose.

La parte audace che penso sia la parte importante. Usa "più impatto" che lo lascia un po 'vago. Lo sto interpretando nel senso che la chiamata Disposein quelle situazioni dovrebbe essere fatta, altrimenti Disposenon è necessario l'uso.


10
Più efficace significa che CTS figlio viene aggiunto a quello genitore. Se non smaltisci un bambino, ci sarà una perdita se il genitore ha una vita lunga. Quindi è fondamentale smaltire quelli collegati.
Grigory

26

Ho dato un'occhiata a ILSpy per il CancellationTokenSourcema posso solo trovare m_KernelEventquale è in realtà un ManualResetEvent, che è una classe wrapper per un WaitHandleoggetto. Questo dovrebbe essere gestito correttamente dal GC.


7
Ho la stessa sensazione che GC risolverà tutto. Proverò a verificarlo. Perché Microsoft ha implementato smaltire in questo caso? Per sbarazzarsi dei callback degli eventi ed evitare probabilmente la propagazione al GC di seconda generazione. In questo caso, chiamare Dispose è facoltativo: chiamalo se puoi, se non semplicemente ignoralo. Non è il modo migliore che penso.
George Mamaladze,

4
Ho studiato questo problema. CancelTokenSource ottiene la raccolta dei rifiuti. Potresti aiutare con smaltimento a farlo in GEN 1 GC. Accettato.
George Mamaladze,

1
Ho fatto la stessa indagine in modo indipendente e sono giunto alla stessa conclusione: smaltisci se puoi facilmente, ma non preoccuparti di provare a farlo nei casi rari ma non inauditi in cui hai inviato una cancellazione. i boondock e non voglio aspettare che scrivano una cartolina che ti dice che hanno finito. Questo accadrà di tanto in tanto a causa della natura di ciò che viene utilizzato per Cancellazione, ed è davvero OK, lo prometto.
Joe Amenta,

6
Il mio commento sopra non si applica alle fonti di token collegate; Non ho potuto dimostrare che è OK lasciare questi indiscussi, e la saggezza in questo thread e MSDN suggerisce che potrebbe non esserlo.
Joe Amenta,

23

Dovresti sempre smaltire CancellationTokenSource.

Come smaltirlo dipende esattamente dallo scenario. Proponi diversi scenari.

  1. usingfunziona solo quando stai usando CancellationTokenSourceun lavoro parallelo che stai aspettando. Se questo è il tuo senario, allora fantastico, è il metodo più semplice.

  2. Quando si utilizzano le attività, utilizzare ContinueWithun'attività come indicato per l'eliminazione CancellationTokenSource.

  3. Per plinq puoi usarlo usingdato che lo stai eseguendo in parallelo ma stai aspettando che tutti gli operatori che eseguono parallelamente finiscano.

  4. Per l'interfaccia utente, è possibile creare un nuovo CancellationTokenSourceper ogni operazione annullabile non legata a un singolo trigger di annullamento. Mantenere un List<IDisposable>e aggiungere ogni sorgente all'elenco, eliminandole tutte quando il componente viene eliminato.

  5. Per i thread, creare un nuovo thread che unisce tutti i thread di lavoro e chiude la singola origine al termine di tutti i thread di lavoro. Vedi CancellazioneTokenSource, Quando smaltire?

C'è sempre un modo. IDisposablele istanze devono essere sempre eliminate. I campioni spesso non lo fanno perché sono esempi rapidi per mostrare l'utilizzo del core o perché l'aggiunta di tutti gli aspetti della classe dimostrata sarebbe eccessivamente complessa per un campione. L'esempio è solo un esempio, non necessariamente (o addirittura di solito) un codice di qualità della produzione. Non tutti i campioni sono accettabili per essere copiati nel codice di produzione così come sono.


per il punto 2, qual è il motivo per cui non è stato possibile utilizzare awaitl'attività e smaltire la forma di cancellazione nel codice che segue l'attesa?
stijn

14
Ci sono avvertimenti. Se il CTS viene annullato durante awaitun'operazione, è possibile riprendere a causa di un OperationCanceledException. Potresti quindi chiamare Dispose(). Ma se ci sono operazioni ancora in esecuzione e utilizzando il corrispondente CancellationToken, quel token continua CanBeCanceleda essere segnalato trueanche se l'origine è eliminata. Se tentano di registrare una richiamata di annullamento, BOOM! , ObjectDisposedException. È abbastanza sicuro chiamare Dispose()dopo il completamento con successo delle operazioni. Diventa davvero complicato quando devi effettivamente cancellare qualcosa.
Mike Strobel,

8
Sottovalutato per i motivi addotti da Mike Strobel: forzare una regola a chiamare sempre Dispose può farti affrontare situazioni pelose quando hai a che fare con CTS e Task a causa della loro natura asincrona. La regola dovrebbe invece essere: eliminare sempre le origini token collegate .
Søren Boisen,

1
Il tuo link va a una risposta eliminata.
Trisped

19

Questa risposta sta ancora arrivando nelle ricerche di Google e credo che la risposta votata non dia la storia completa. Dopo aver esaminato il codice sorgente per CancellationTokenSource(CTS) e CancellationToken(CT), credo che per la maggior parte dei casi d'uso vada bene la seguente sequenza di codice:

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

Il m_kernelHandlecampo interno sopra menzionato è l'oggetto di sincronizzazione che supporta la WaitHandleproprietà nelle classi CTS e CT. Viene istanziato solo se si accede a quella proprietà. Quindi, a meno che tu non stia usando WaitHandleper qualche sincronizzazione di thread vecchia scuola nel tuoTask chiamate, non si avrà alcun effetto.

Ovviamente, se lo stai usando, dovresti fare ciò che è suggerito dalle altre risposte sopra e ritardare la chiamata Disposefino al completamento di qualsiasi WaitHandleoperazione che utilizza l'handle, perché, come descritto nella documentazione dell'API di Windows per WaitHandle , i risultati non sono definiti.


7
L'articolo MSDN Cancellazione nei thread gestiti afferma: "I listener monitorano il valore della IsCancellationRequestedproprietà del token tramite polling, callback o handle di attesa". In altre parole: potresti non essere tu (cioè colui che effettua la richiesta asincrona) che usa l'handle wait, potrebbe essere l'ascoltatore (cioè colui che risponde alla richiesta). Il che significa che tu, in quanto responsabile dello smaltimento, non hai alcun controllo sull'uso o meno della maniglia di attesa.
Herzbube,

Secondo MSDN, i callback registrati che hanno fatto eccezione hanno causato il lancio di .Cancel. Il tuo codice non chiamerà .Dispose () se questo accade. I callback dovrebbero stare attenti a non farlo, ma può succedere.
Joseph Lennox,

11

È passato molto tempo da quando l'ho chiesto e ho ottenuto molte risposte utili, ma mi sono imbattuto in un problema interessante relativo a questo e ho pensato di pubblicarlo qui come un'altra risposta:

Dovresti chiamare CancellationTokenSource.Dispose()solo quando sei sicuro che nessuno tenterà di ottenere la Tokenproprietà del CTS . In caso contrario, si dovrebbe non chiami, perché è una gara. Ad esempio, vedi qui:

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

Nella correzione di questo problema, il codice che in precedenza è cts.Cancel(); cts.Dispose();stato modificato è stato modificato semplicemente cts.Cancel();perché qualcuno così sfortunato da tentare di ottenere il token di annullamento per osservare il suo stato di annullamento dopo che Dispose è stato chiamato dovrà purtroppo anche gestire ObjectDisposedException- oltre al OperationCanceledExceptionper cui stavano pianificando.

Un'altra osservazione chiave correlata a questa correzione è fatta da Tratcher: "Lo smaltimento è richiesto solo per i token che non verranno annullati, poiché la cancellazione fa la stessa pulizia". vale a dire semplicemente fare Cancel()invece di smaltire è davvero abbastanza buono!


1

Ho creato una classe thread-safe che lega a CancellationTokenSourcea a Taske garantisce che la CancellationTokenSourcevolontà verrà eliminata al termine della relativa associazione Task. Utilizza i blocchi per garantire che CancellationTokenSourcenon vengano cancellati durante o dopo lo smaltimento. Questo accade per conformità con la documentazione , che afferma:

Il Disposemetodo deve essere utilizzato solo quando tutte le altre operazioni CancellationTokenSourcesull'oggetto sono state completate.

E anche :

Il Disposemetodo lascia lo CancellationTokenSourcestato inutilizzabile.

Ecco la 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;
}

I metodi principali della CancelableExecutionclasse sono il RunAsynce il Cancel. Per impostazione predefinita, le operazioni simultanee non sono consentite, ovvero la chiamataRunAsync una seconda volta annullerà silenziosamente e attenderà il completamento dell'operazione precedente (se è ancora in esecuzione), prima di iniziare la nuova operazione.

Questa classe può essere utilizzata in applicazioni di qualsiasi tipo. Il suo utilizzo principale è tuttavia nelle applicazioni UI, all'interno di moduli con pulsanti per l'avvio e l'annullamento di un'operazione asincrona o con una casella di riepilogo che annulla e riavvia un'operazione ogni volta che viene modificato l'elemento selezionato. Ecco un esempio del primo 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();
}

Il RunAsyncmetodo accetta un extra CancellationTokencome argomento, che è collegato al creato internamente CancellationTokenSource. Fornire questo token opzionale può essere utile in scenari di avanzamento.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.