Wann muss CancellationTokenSource entsorgt werden?

163

Die Klasse CancellationTokenSourceist verfügbar. Ein kurzer Blick in Reflector zeigt die Verwendung KernelEventeiner (sehr wahrscheinlich) nicht verwalteten Ressource. Da CancellationTokenSourcees keinen Finalizer gibt, wird der GC dies nicht tun, wenn wir ihn nicht entsorgen.

Wenn Sie sich dagegen die Beispiele ansehen, die im MSDN-Artikel Abbrechen in verwalteten Threads aufgeführt sind , verfügt nur ein Code-Snippet über das Token.

Was ist der richtige Weg, um es im Code zu entsorgen?

  1. Sie können Code, mit dem usingSie Ihre parallele Aufgabe starten, nicht umbrechen, wenn Sie nicht darauf warten. Und eine Stornierung ist nur dann sinnvoll, wenn Sie nicht warten.
  2. Natürlich können Sie ContinueWithmit einem DisposeAnruf eine Aufgabe hinzufügen , aber ist das der richtige Weg?
  3. Was ist mit stornierbaren PLINQ-Abfragen, die nicht zurücksynchronisieren, sondern am Ende nur etwas tun? Sagen wir .ForAll(x => Console.Write(x))?
  4. Ist es wiederverwendbar? Kann dasselbe Token für mehrere Aufrufe verwendet werden und dann zusammen mit der Hostkomponente entsorgt werden, z. B. UI-Steuerung?

Da es so etwas wie eine ResetBereinigungs- IsCancelRequestedund TokenFeldmethode nicht gibt, würde ich annehmen, dass es nicht wiederverwendbar ist. Daher sollten Sie jedes Mal, wenn Sie eine Aufgabe (oder eine PLINQ-Abfrage) starten, eine neue erstellen. Ist es wahr? Wenn ja, ist meine Frage, welche Strategie in Disposediesen vielen CancellationTokenSourceFällen richtig und empfohlen ist .

George Mamaladze
quelle

Antworten:

81

Apropos, ob es wirklich notwendig ist, Dispose aufzurufen CancellationTokenSource... Ich hatte einen Speicherverlust in meinem Projekt und es stellte sich heraus, dass dies CancellationTokenSourcedas Problem war.

Mein Projekt verfügt über einen Dienst, der ständig Datenbanken liest und verschiedene Aufgaben auslöst. Ich habe verknüpfte Stornierungs-Token an meine Mitarbeiter übergeben. Selbst nachdem sie die Datenverarbeitung abgeschlossen hatten, wurden Stornierungs-Token nicht entsorgt, was zu einem Speicherverlust führte.

Die MSDN- Stornierung in verwalteten Threads gibt Folgendes klar an:

Beachten Sie, dass Sie anrufen müssen Dispose die verknüpfte Tokenquelle wenn Sie damit fertig sind. Ein vollständigeres Beispiel finden Sie unter Gewusst wie: Auf mehrere Stornierungsanforderungen warten .

Ich habe ContinueWithin meiner Implementierung verwendet.

Gruzilkin
quelle
14
Dies ist eine wichtige Lücke in der aktuell akzeptierten Antwort von Bryan Crosby. Wenn Sie ein verknüpftes CTS erstellen , besteht die Gefahr von Speicherlecks. Das Szenario ist Ereignishandlern sehr ähnlich, die niemals nicht registriert sind.
Søren Boisen
5
Ich hatte ein Leck aufgrund des gleichen Problems. Mit einem Profiler konnte ich Rückrufregistrierungen sehen, die Verweise auf die verknüpften CTS-Instanzen enthielten. Die Prüfung des Codes für die Implementierung von CTS Dispose hier war sehr aufschlussreich und unterstreicht den Vergleich von @ SørenBoisen mit Registrierungslecks bei der Ereignisbehandlungsroutine.
BitMask777
Die obigen Kommentare spiegeln den Diskussionsstatus wider, in dem die andere Antwort von @Bryan Crosby akzeptiert wurde.
George Mamaladze
In der Dokumentation von 2020 heißt es eindeutig: 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

Ich fand keine der aktuellen Antworten zufriedenstellend. Nach Recherchen fand ich diese Antwort von Stephen Toub ( Referenz ):

Es hängt davon ab, ob. In .NET 4 diente CTS.Dispose zwei Hauptzwecken. Wenn auf das WaitHandle des CancellationToken zugegriffen wurde (wodurch es träge zugewiesen wird), wird dieses Handle von Dispose entsorgt. Wenn der CTS über die CreateLinkedTokenSource-Methode erstellt wurde, hebt Dispose den CTS von den Token auf, mit denen er verknüpft war. In .NET 4.5 hat Dispose einen zusätzlichen Zweck: Wenn der CTS einen Timer unter der Decke verwendet (z. B. CancelAfter wurde aufgerufen), wird der Timer entsorgt.

Es ist sehr selten, dass CancellationToken.WaitHandle verwendet wird. Daher ist das Aufräumen nach dem Aufräumen normalerweise kein guter Grund, Dispose zu verwenden. Wenn Sie Ihr CTS jedoch mit CreateLinkedTokenSource erstellen oder die Timer-Funktionalität des CTS verwenden, kann die Verwendung von Dispose effektiver sein.

Der kühne Teil, denke ich, ist der wichtige Teil. Er verwendet "wirkungsvoller", was es etwas vage lässt. Ich interpretiere es so, dass das Aufrufen Disposein solchen Situationen erfolgen sollte, andernfalls ist die Verwendung Disposenicht erforderlich.

Jesse Gut
quelle
10
Effektiver bedeutet, dass das untergeordnete CTS dem übergeordneten CTS hinzugefügt wird. Wenn Sie das Kind nicht entsorgen, tritt ein Leck auf, wenn die Eltern langlebig sind. Daher ist es wichtig, verknüpfte zu entsorgen.
Grigory
26

Ich habe in ILSpy nach dem gesucht, CancellationTokenSourceaber ich kann nur herausfinden, m_KernelEventwelches tatsächlich ein ist ManualResetEvent, welches eine Wrapper-Klasse für ein WaitHandleObjekt ist. Dies sollte vom GC ordnungsgemäß gehandhabt werden.

Bryan Crosby
quelle
7
Ich habe das gleiche Gefühl, dass GC das alles bereinigen wird. Ich werde versuchen, das zu überprüfen. Warum implementiert Microsoft in diesem Fall Dispose? Um Ereignisrückrufe loszuwerden und die Weitergabe an GC der zweiten Generation wahrscheinlich zu vermeiden. In diesem Fall ist das Aufrufen von Dispose optional - rufen Sie es auf, wenn Sie können, oder ignorieren Sie es einfach. Nicht die beste Art, denke ich.
George Mamaladze
4
Ich habe dieses Problem untersucht. CancellationTokenSource sammelt Müll. Sie können bei der Entsorgung helfen, um dies in GEN 1 GC zu tun. Akzeptiert.
George Mamaladze
1
Ich habe dieselbe Untersuchung unabhängig durchgeführt und bin zu dem gleichen Ergebnis gekommen: Entsorgen Sie, wenn Sie es leicht können, aber ärgern Sie sich nicht darüber, dies in den seltenen, aber nicht unerhörten Fällen zu versuchen, in denen Sie ein CancellationToken gesendet haben die Boondocks und wollen nicht darauf warten, dass sie eine Postkarte zurückschreiben, die Ihnen sagt, dass sie damit fertig sind. Dies wird von Zeit zu Zeit passieren, da CancellationToken verwendet wird, und es ist wirklich in Ordnung, das verspreche ich.
Joe Amenta
6
Mein obiger Kommentar gilt nicht für verknüpfte Tokenquellen. Ich konnte nicht beweisen, dass es in Ordnung ist, diese nicht verfügbar zu machen, und die Weisheit in diesem Thread und MSDN legt nahe, dass dies möglicherweise nicht der Fall ist.
Joe Amenta
23

Sie sollten immer entsorgen CancellationTokenSource.

Wie es zu entsorgen ist, hängt genau vom Szenario ab. Sie schlagen verschiedene Szenarien vor.

  1. usingFunktioniert nur, wenn Sie CancellationTokenSourceparallele Arbeiten ausführen, auf die Sie warten. Wenn das dein Senario ist, dann ist es großartig, es ist die einfachste Methode.

  2. Verwenden ContinueWithSie bei der Verwendung von Aufgaben eine Aufgabe, die Sie angegeben haben, um sie zu entsorgen CancellationTokenSource.

  3. Für plinq können Sie verwenden, usingda Sie es parallel ausführen, aber darauf warten, dass alle parallel ausgeführten Mitarbeiter fertig sind.

  4. Für die Benutzeroberfläche können Sie CancellationTokenSourcefür jede stornierbare Operation eine neue erstellen , die nicht an einen einzelnen Abbruchauslöser gebunden ist. Pflegen Sie a List<IDisposable>und fügen Sie jede Quelle zur Liste hinzu. Entsorgen Sie alle, wenn Ihre Komponente entsorgt wird.

  5. Erstellen Sie für Threads einen neuen Thread, der alle Worker-Threads verbindet und die einzelne Quelle schließt, wenn alle Worker-Threads abgeschlossen sind. Siehe CancellationTokenSource, Wann entsorgen?

Es gibt immer einen Weg. IDisposableInstanzen sollten immer entsorgt werden. Beispiele sind oft nicht vorhanden, weil sie entweder schnelle Beispiele sind, um die Kernnutzung zu zeigen, oder weil das Hinzufügen aller Aspekte der demonstrierten Klasse für ein Beispiel zu komplex wäre. Die Stichprobe ist nur eine Stichprobe, nicht unbedingt (oder sogar normalerweise) Produktionsqualitätscode. Es ist nicht akzeptabel, dass alle Muster unverändert in den Produktionscode kopiert werden.

Samuel Neff
quelle
Gibt es für Punkt 2 einen Grund, den Sie awaitfür die Aufgabe nicht verwenden und die CancellationTokenSource in dem Code entsorgen konnten, der nach dem Warten kommt?
14.
14
Es gibt Vorbehalte. Wenn der CTS während awaiteiner Operation abgebrochen wird , können Sie aufgrund eines Vorgangs fortfahren OperationCanceledException. Sie könnten dann anrufen Dispose(). Wenn jedoch noch Vorgänge ausgeführt werden und das entsprechende verwendet wird CancellationToken, wird dieses Token weiterhin CanBeCanceledals vorhanden gemeldet true, obwohl die Quelle entsorgt ist. Wenn sie versuchen, einen Stornierungsrückruf zu registrieren, wird BOOM! , ObjectDisposedException. Es ist sicher genug, Dispose()nach erfolgreichem Abschluss der Operation (en) anzurufen . Es wird sehr schwierig, wenn Sie tatsächlich etwas abbrechen müssen.
Mike Strobel
8
Aus den von Mike Strobel angegebenen Gründen herabgestimmt - das Erzwingen einer Regel, immer Dispose aufzurufen, kann Sie aufgrund ihrer asynchronen Natur in haarige Situationen bringen, wenn Sie mit CTS und Task umgehen. Die Regel sollte stattdessen lauten: Entsorgen Sie immer verknüpfte Tokenquellen.
Søren Boisen
1
Ihr Link führt zu einer gelöschten Antwort.
Trisped
19

Diese Antwort taucht immer noch in der Google-Suche auf, und ich glaube, dass die abgestimmte Antwort nicht die ganze Geschichte wiedergibt. Nachdem ich mir den Quellcode für CancellationTokenSource(CTS) und CancellationToken(CT) angesehen habe, glaube ich, dass für die meisten Anwendungsfälle die folgende Codesequenz in Ordnung ist:

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

Das m_kernelHandleoben erwähnte interne Feld ist das Synchronisationsobjekt, das die WaitHandleEigenschaft sowohl in der CTS- als auch in der CT-Klasse unterstützt. Es wird nur instanziiert, wenn Sie auf diese Eigenschaft zugreifen. Wenn Sie also keine WaitHandleThread-Synchronisierung der alten Schule in Ihrer Taskaufrufenden Entsorgung verwenden, hat dies keine Auswirkungen.

Wenn Sie es verwenden, sollten Sie natürlich das tun, was in den anderen Antworten oben vorgeschlagen wird, und den Aufruf verzögern, Disposebis alle WaitHandleVorgänge mit dem Handle abgeschlossen sind, da die Ergebnisse , wie in der Windows-API-Dokumentation für WaitHandle beschrieben , undefiniert sind.

Jlyonsmith
quelle
7
In dem MSDN-Artikel " Abbrechen in verwalteten Threads" heißt es: "Listener überwachen den Wert der IsCancellationRequestedEigenschaft des Tokens durch Abfragen, Rückrufen oder Warten." Mit anderen Worten: Möglicherweise verwenden nicht Sie (dh derjenige, der die asynchrone Anforderung stellt) das Wartehandle, sondern möglicherweise der Listener (dh derjenige, der die Anforderung beantwortet). Dies bedeutet, dass Sie als Verantwortlicher für die Entsorgung praktisch keine Kontrolle darüber haben, ob der Wartegriff verwendet wird oder nicht.
Herzbube
Laut MSDN führen registrierte Rückrufe mit Ausnahmefällen dazu, dass .Cancel ausgelöst wird. Ihr Code ruft in diesem Fall nicht .Dispose () auf. Die Rückrufe sollten darauf achten, dies nicht zu tun, aber es kann passieren.
Joseph Lennox
11

Es ist lange her, dass ich dies gefragt und viele hilfreiche Antworten erhalten habe, aber ich bin auf ein interessantes Problem gestoßen und dachte, ich würde es hier als eine andere Art von Antwort posten:

Sie sollten CancellationTokenSource.Dispose()nur anrufen , wenn Sie sicher sind, dass niemand versuchen wird, das TokenEigentum des CTS zu erhalten. Ansonsten solltest du es nicht nennen, weil es ein Rennen ist. Zum Beispiel hier:

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

In der Korrektur für dieses Problem wurde der Code, der zuvor cts.Cancel(); cts.Dispose();bearbeitet wurde, nur bearbeitet, cts.Cancel();weil jeder, der so viel Pech hatte, das Stornierungs-Token zu erhalten, um seinen Stornierungsstatus nach dem Dispose Aufruf zu beobachten, leider auch behandelt werden muss ObjectDisposedException- zusätzlich zu demOperationCanceledException dass sie geplant hatten.

Eine weitere wichtige Bemerkung zu diesem Fix wurde von Tratcher gemacht: "Die Entsorgung ist nur für Token erforderlich, die nicht storniert werden, da die Stornierung alle die gleiche Bereinigung bewirkt." dh nur zu tun Cancel()statt zu entsorgen ist wirklich gut genug!

Tim Lovell-Smith
quelle
1

Ich habe eine thread-sichere Klasse erstellt, die a CancellationTokenSourcean a bindet Taskund garantiert, dass die Klasse CancellationTokenSourceentsorgt wird, wenn die zugehörige Taskabgeschlossen ist. Es verwendet Schlösser, um sicherzustellen, dass das CancellationTokenSourceProdukt während oder nach der Entsorgung nicht gelöscht wird. Dies geschieht zur Einhaltung der Dokumentation , in der Folgendes angegeben ist:

Die DisposeMethode darf nur verwendet werden, wenn alle anderen Vorgänge für das CancellationTokenSourceObjekt abgeschlossen sind.

Und auch :

Die DisposeMethode belässt das CancellationTokenSourcein einem unbrauchbaren Zustand.

Hier ist die Klasse:

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

Die primären Methoden der CancelableExecutionKlasse sind die RunAsyncund die Cancel. Standardmäßig sind gleichzeitige Vorgänge nicht zulässig, dh das AufrufenRunAsync ein zweites stillschweigend abgebrochen wird und auf den Abschluss des vorherigen Vorgangs (sofern dieser noch ausgeführt wird) gewartet wird, bevor der neue Vorgang gestartet wird.

Diese Klasse kann in Anwendungen jeglicher Art verwendet werden. Die Hauptverwendung erfolgt jedoch in UI-Anwendungen, in Formularen mit Schaltflächen zum Starten und Abbrechen eines asynchronen Vorgangs oder in einem Listenfeld, das einen Vorgang jedes Mal abbricht und neu startet, wenn das ausgewählte Element geändert wird. Hier ist ein Beispiel für den ersten Fall:

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();
}

Die RunAsyncMethode akzeptiert ein Extra CancellationTokenals Argument, das mit dem intern erstellten verknüpft ist CancellationTokenSource. Das Bereitstellen dieses optionalen Tokens kann in Fortschrittsszenarien hilfreich sein.

Theodor Zoulias
quelle