¿Se utiliza la forma correcta de cancelar un token de cancelación en una tarea?

10

Tengo un código que crea un token de cancelación

public partial class CardsTabViewModel : BaseViewModel
{
   public CancellationTokenSource cts;

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

Código que lo usa:

await GetCards(cts.Token);


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

y el código que luego cancela este token de cancelación si el usuario se aleja de la pantalla donde se está ejecutando el código anterior:

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

Con respecto a la cancelación, ¿es esta la forma correcta de cancelar el token cuando se usa en una Tarea?

En particular revisé esta pregunta:

Uso de la propiedad IsCancellationRequested?

y me hace pensar que no estoy haciendo la cancelación de la manera correcta o tal vez de una manera que pueda causar una excepción.

Además, en este caso, después de haber cancelado, ¿debería hacer un cts.Dispose ()?

Alan2
fuente
Normalmente, use el método Cancel para comunicar una solicitud de cancelación y luego use el método Dispose para liberar la memoria. Puede consultar la muestra en el enlace. docs.microsoft.com/en-us/dotnet/api/…
Wendy Zang - MSFT

Respuestas:

2

CancellationTokenSource.Cancel() es una forma válida de comenzar la cancelación.

El sondeo ct.IsCancellationRequestedevita el lanzamiento OperationCanceledException. Debido a su sondeo, requiere una iteración del ciclo para completarse antes de que responda a la solicitud de cancelación.

Si GetViewablePhrases()y CheckAvailability()puede modificarse para aceptar a CancellationToken, esto puede hacer que la cancelación sea más rápida para responder, a costa de haberla OperationCanceledExceptionarrojado.

"¿debería estar haciendo un cts.Dispose ()?" no es tan sencillo ...

"Deseche siempre los desechables ID lo antes posible"

Es más una guía que una regla. Tasken sí mismo es desechable, pero casi nunca está directamente dispuesto en código.

Hay casos (cuando WaitHandlese utilizan controladores de devolución de llamada o cancelación) en los que la eliminación ctsliberaría un recurso / eliminaría una raíz de GC que de otro modo solo sería liberada por un Finalizador. Estos no se aplican a su código tal como está, pero pueden hacerlo en el futuro.

Agregar una llamada a Disposedespués de cancelar garantizaría que estos recursos se liberen rápidamente en futuras versiones del código.

Sin embargo, tendría que esperar a que ctstermine el código que usa antes de llamar a disposed, o modificar el código para tratar el ObjectDisposedExceptionuso de cts(o su token) después de la eliminación.

Peter Wishart
fuente
"conectar OnDisappearing para deshacerse de los cts" Parece una muy mala idea, porque todavía está en uso dentro de otra tarea. Particularmente si alguien cambia el diseño más tarde (modifica las subtareas para aceptar un CancellationTokenparámetro), podría estar desechando WaitHandlemientras otro hilo lo está esperando activamente :(
Ben Voigt
1
En particular, debido a que usted hizo la afirmación de que "cancelar realiza la misma limpieza que desechar", sería inútil llamar Disposedesde OnDisappearing.
Ben Voigt
Vaya, me perdí que el código en la respuesta ya llama Cancel...
Peter Wishart
He eliminado el reclamo sobre cancelar haciendo la misma limpieza (que había leído en otra parte), por lo que puedo decir, la única limpieza que Cancelhace es el temporizador interno (si se usa).
Peter Wishart
3

En general, veo un uso justo de Cancelar token en su código, pero de acuerdo con el Patrón de sincronización de tareas, su código podría no cancelarse de inmediato.

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

Para responder de inmediato, el código de bloqueo también debe cancelarse

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

Depende de usted si debe Eliminar, si hay muchos recursos de memoria reservados en el código interrumpido, debe hacerlo.

Fidel Orozco
fuente
1
Y, de hecho, esto también se aplicaría a la llamada a GetViewablePhrases; idealmente, también sería una llamada asincrónica y recibiría un token de cancelación como una opción.
Paddy
1

Le recomendaría que eche un vistazo a una de las clases .net para comprender completamente cómo manejar los métodos de espera con CanncelationToken, recogí SeamaphoreSlim.cs

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

También puede ver toda la clase aquí, https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,6095d9030263f169

Muhab
fuente