Ist der richtige Weg, ein in einer Aufgabe verwendetes Stornierungs-Token abzubrechen?

10

Ich habe Code, der ein Stornierungs-Token erstellt

public partial class CardsTabViewModel : BaseViewModel
{
   public CancellationTokenSource cts;

public async Task OnAppearing()
{
   cts = new CancellationTokenSource(); // << runs as part of OnAppearing()

Code, der es verwendet:

await GetCards(cts.Token);


public async Task GetCards(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        App.viewablePhrases = App.DB.GetViewablePhrases(Settings.Mode, Settings.Pts);
        await CheckAvailability();
    }
}

und Code, der dieses Stornierungs-Token später abbricht, wenn sich der Benutzer von dem Bildschirm entfernt, auf dem der obige Code ausgeführt wird:

public void OnDisappearing()
{
   cts.Cancel();

Ist dies in Bezug auf das Abbrechen die richtige Methode, um das Token abzubrechen, wenn es in einer Aufgabe verwendet wird?

Insbesondere habe ich diese Frage geprüft:

Verwendung der IsCancellationRequested-Eigenschaft?

und es lässt mich denken, dass ich den Abbruch nicht richtig oder vielleicht auf eine Weise mache, die eine Ausnahme verursachen kann.

Sollte ich in diesem Fall nach dem Abbruch eine cts.Dispose () ausführen?

Alan2
quelle
Verwenden Sie normalerweise die Cancel-Methode, um eine Stornierungsanforderung zu übermitteln, und verwenden Sie dann die Dispose-Methode, um den Speicher freizugeben. Sie können das Beispiel im Link überprüfen. docs.microsoft.com/en-us/dotnet/api/…
Wendy Zang - MSFT

Antworten:

2

CancellationTokenSource.Cancel() ist ein gültiger Weg, um die Stornierung zu starten.

Polling ct.IsCancellationRequestedvermeidet das Werfen OperationCanceledException. Aufgrund der Abfrage muss die Schleife wiederholt werden, bevor sie auf die Stornierungsanforderung reagiert.

Wenn GetViewablePhrases()undCheckAvailability() geändert werden kann, um a zu akzeptieren CancellationToken, kann dies dazu führen, dass die Stornierung schneller erfolgt, und zwar auf Kosten vonOperationCanceledException Werfens.

"sollte ich ein cts.Dispose () machen?" ist nicht so einfach ...

"IDisposables immer so schnell wie möglich entsorgen"

Ist eher eine Richtlinie als eine Regel. Taskselbst ist wegwerfbar, aber kaum jemals direkt im Code angeordnet.

Es gibt Fälle (wenn WaitHandle Rückruf-Handler verwendet werden oder die abgebrochen werden), in denen durch das Entsorgen ctseine Ressource freigegeben / ein GC-Stamm entfernt wird, der sonst nur von einem Finalizer freigegeben würde. Diese gelten nicht für Ihren aktuellen Code, können aber in Zukunft gelten.

Anruf hinzufügen zu Dispose nach dem Abbrechen würde sicherstellen, dass diese Ressourcen in zukünftigen Versionen des Codes sofort freigegeben werden.

Sie müssen jedoch entweder warten, bis der verwendete Code ctsbeendet ist, bevor Sie dispose aufrufen, oder den Code ändern, der nach der Entsorgung ObjectDisposedExceptionvon der Verwendung von cts(oder seinem Token) verarbeitet werden soll.

Peter Wishart
quelle
"OnDisappearing anschließen, um cts zu entsorgen" Scheint eine sehr schlechte Idee zu sein, da es in einer anderen Aufgabe noch verwendet wird. Insbesondere wenn jemand später das Design ändert (die Unteraufgaben ändern, um einen CancellationTokenParameter zu akzeptieren ), könnten Sie das entsorgen, WaitHandlewährend ein anderer Thread aktiv darauf wartet :(
Ben Voigt
1
Insbesondere , weil Sie den Anspruch geltend gemacht , dass „führt die gleiche Bereinigung wie dispose Abbrechen“, wäre es sinnlos, nennen Disposeaus OnDisappearing.
Ben Voigt
Hoppla, ich habe verpasst, dass der Code in der Antwort bereits anruft Cancel...
Peter Wishart
Habe die Behauptung über das Abbrechen derselben Bereinigung (die ich an anderer Stelle gelesen habe) gelöscht, soweit ich das beurteilen kann, ist die einzige Bereinigung Cancelder interne Timer (falls verwendet).
Peter Wishart
3

Im Allgemeinen sehe ich eine faire Verwendung von Cancel Token in Ihrem Code, aber gemäß dem Task Async Pattern kann Ihr Code möglicherweise nicht sofort abgebrochen werden.

while (!ct.IsCancellationRequested)
{
   App.viewablePhrases = App.DB.GetViewablePhrases(Settings.Mode, Settings.Pts);
   await CheckAvailability();   //Your Code could be blocked here, unable to cancel
}

Um sofort zu antworten, sollte auch der Sperrcode aufgehoben werden

await CheckAvailability(ct);   //Your blocking code in the loop also should be stoped

Es liegt an Ihnen, ob Sie entsorgen müssen. Wenn im unterbrochenen Code viele Speicherressourcen reserviert sind, sollten Sie dies tun.

Fidel Orozco
quelle
1
Dies würde in der Tat auch für den Aufruf von GetViewablePhrases gelten - im Idealfall wäre dies auch ein asynchroner Aufruf, bei dem optional ein Stornierungs-Token verwendet wird.
Paddy
1

Ich würde Ihnen empfehlen, einen Blick auf eine der .net-Klassen zu werfen, um zu verstehen, wie man mit Wartemethoden mit CanncelationToken umgeht. Ich habe SeamaphoreSlim.cs ausgewählt

    public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
    {
        CheckDispose();

        // Validate input
        if (millisecondsTimeout < -1)
        {
            throw new ArgumentOutOfRangeException(
                "totalMilliSeconds", millisecondsTimeout, GetResourceString("SemaphoreSlim_Wait_TimeoutWrong"));
        }

        cancellationToken.ThrowIfCancellationRequested();

        uint startTime = 0;
        if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0)
        {
            startTime = TimeoutHelper.GetTime();
        }

        bool waitSuccessful = false;
        Task<bool> asyncWaitTask = null;
        bool lockTaken = false;

        //Register for cancellation outside of the main lock.
        //NOTE: Register/deregister inside the lock can deadlock as different lock acquisition orders could
        //      occur for (1)this.m_lockObj and (2)cts.internalLock
        CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.InternalRegisterWithoutEC(s_cancellationTokenCanceledEventHandler, this);
        try
        {
            // Perf: first spin wait for the count to be positive, but only up to the first planned yield.
            //       This additional amount of spinwaiting in addition
            //       to Monitor.Enter()’s spinwaiting has shown measurable perf gains in test scenarios.
            //
            SpinWait spin = new SpinWait();
            while (m_currentCount == 0 && !spin.NextSpinWillYield)
            {
                spin.SpinOnce();
            }
            // entering the lock and incrementing waiters must not suffer a thread-abort, else we cannot
            // clean up m_waitCount correctly, which may lead to deadlock due to non-woken waiters.
            try { }
            finally
            {
                Monitor.Enter(m_lockObj, ref lockTaken);
                if (lockTaken)
                {
                    m_waitCount++;
                }
            }

            // If there are any async waiters, for fairness we'll get in line behind
            // then by translating our synchronous wait into an asynchronous one that we 
            // then block on (once we've released the lock).
            if (m_asyncHead != null)
            {
                Contract.Assert(m_asyncTail != null, "tail should not be null if head isn't");
                asyncWaitTask = WaitAsync(millisecondsTimeout, cancellationToken);
            }
                // There are no async waiters, so we can proceed with normal synchronous waiting.
            else
            {
                // If the count > 0 we are good to move on.
                // If not, then wait if we were given allowed some wait duration

                OperationCanceledException oce = null;

                if (m_currentCount == 0)
                {
                    if (millisecondsTimeout == 0)
                    {
                        return false;
                    }

                    // Prepare for the main wait...
                    // wait until the count become greater than zero or the timeout is expired
                    try
                    {
                        waitSuccessful = WaitUntilCountOrTimeout(millisecondsTimeout, startTime, cancellationToken);
                    }
                    catch (OperationCanceledException e) { oce = e; }
                }

                // Now try to acquire.  We prioritize acquisition over cancellation/timeout so that we don't
                // lose any counts when there are asynchronous waiters in the mix.  Asynchronous waiters
                // defer to synchronous waiters in priority, which means that if it's possible an asynchronous
                // waiter didn't get released because a synchronous waiter was present, we need to ensure
                // that synchronous waiter succeeds so that they have a chance to release.
                Contract.Assert(!waitSuccessful || m_currentCount > 0, 
                    "If the wait was successful, there should be count available.");
                if (m_currentCount > 0)
                {
                    waitSuccessful = true;
                    m_currentCount--;
                }
                else if (oce != null)
                {
                    throw oce;
                }

                // Exposing wait handle which is lazily initialized if needed
                if (m_waitHandle != null && m_currentCount == 0)
                {
                    m_waitHandle.Reset();
                }
            }
        }
        finally
        {
            // Release the lock
            if (lockTaken)
            {
                m_waitCount--;
                Monitor.Exit(m_lockObj);
            }

            // Unregister the cancellation callback.
            cancellationTokenRegistration.Dispose();
        }

        // If we had to fall back to asynchronous waiting, block on it
        // here now that we've released the lock, and return its
        // result when available.  Otherwise, this was a synchronous
        // wait, and whether we successfully acquired the semaphore is
        // stored in waitSuccessful.

        return (asyncWaitTask != null) ? asyncWaitTask.GetAwaiter().GetResult() : waitSuccessful;
    }

Sie können die gesamte Klasse auch hier anzeigen: https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,6095d9030263f169

Muhab
quelle