A maneira correta de cancelar um token de cancelamento é usada em uma tarefa?

10

Eu tenho um código que cria um token de cancelamento

public partial class CardsTabViewModel : BaseViewModel
{
   public CancellationTokenSource cts;

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

Código que o utiliza:

await GetCards(cts.Token);


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

e código que mais tarde cancela esse token de cancelamento se o usuário se afastar da tela em que o código acima está sendo executado:

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

Em relação ao cancelamento, esta é a maneira correta de cancelar o token quando ele está sendo usado em uma tarefa?

Em particular, verifiquei esta pergunta:

Uso da propriedade IsCancellationRequested?

e está me fazendo pensar que não estou cancelando da maneira correta ou talvez de uma maneira que possa causar uma exceção.

Além disso, nesse caso, depois que cancelei, devo fazer um cts.Dispose ()?

Alan2
fonte
Normalmente, use o método Cancel para comunicar uma solicitação de cancelamento e use o método Dispose para liberar a memória. Você pode verificar a amostra no link. docs.microsoft.com/en-us/dotnet/api/…
Wendy Zang - MSFT

Respostas:

2

CancellationTokenSource.Cancel() é uma maneira válida de iniciar o cancelamento.

A votação ct.IsCancellationRequestedevita jogar OperationCanceledException. Devido à sua pesquisa, é necessário que uma iteração do loop seja concluída antes de responder à solicitação de cancelamento.

Se GetViewablePhrases()e CheckAvailability()puder ser modificado para aceitar a CancellationToken, isso poderá acelerar a resposta do cancelamento, ao custo de ter sido OperationCanceledExceptionlançado.

"eu deveria estar fazendo um cts.Dispose ()?" não é tão simples ...

"Sempre descarte IDisposables o mais rápido possível"

É mais uma diretriz do que uma regra. Taskele próprio é descartável, mas quase nunca é descartado diretamente no código.

Existem casos (quando WaitHandleou manipuladores de retorno de chamada de cancelamento são usados) em que o descarte liberaria ctsum recurso / removeria uma raiz de GC que, de outra forma, seria liberada apenas por um Finalizador. Eles não se aplicam ao seu código como está, mas podem ser no futuro.

A adição de uma chamada Disposeapós o cancelamento garantiria que esses recursos fossem liberados imediatamente em versões futuras do código.

No entanto, é necessário aguardar o código que ctstermina antes de chamar o descarte ou modificar o código para lidar com o ObjectDisposedExceptionuso de cts(ou seu token) após o descarte.

Peter Wishart
fonte
"ligar OnDisappearing para descartar cts" Parece uma péssima idéia, porque ainda está em uso em outra tarefa. Particularmente, se alguém alterar posteriormente o design (modifique as subtarefas para aceitar um CancellationTokenparâmetro), você poderá descartar WaitHandleenquanto outro thread o aguarda ativamente :(
Ben Voigt
11
Em particular, como você alegou que "o cancelamento executa a mesma limpeza que o descarte", não faria sentido telefonar Disposede OnDisappearing.
Ben Voigt
Opa, eu perdi que o código na resposta já chama Cancel...
Peter Wishart
Excluímos a reivindicação de cancelamento da mesma limpeza (que eu já havia lido em outros lugares), tanto quanto posso dizer que a única limpeza Cancelé o temporizador interno (se usado).
Peter Wishart
3

Em geral, vejo um uso justo do token de cancelamento no seu código, mas, de acordo com o padrão de tarefas assíncronas, seu código não pode ser cancelado imediatamente.

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 imediatamente, o código de bloqueio também deve ser cancelado

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

Depende de você se você deve Dispose, se houver muitos recursos de memória reservados no código interrompido, você deve fazê-lo.

Fidel Orozco
fonte
11
E, de fato, isso também se aplicaria à chamada para GetViewablePhrases - idealmente, também seria uma chamada assíncrona e aceitaria um token de cancelamento como opção.
Paddy
1

Eu recomendo que você dê uma olhada em uma das classes .net para entender completamente como lidar com métodos de espera com CanncelationToken, peguei 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;
    }

Você também pode visualizar toda a classe aqui, https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,6095d9030263f169

Muhab
fonte