Правильный ли способ отмены токена отмены используется в задании?

10

У меня есть код, который создает токен отмены

public partial class CardsTabViewModel : BaseViewModel
{
   public CancellationTokenSource cts;

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

Код, который использует это:

await GetCards(cts.Token);


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

и код, который впоследствии отменяет этот токен отмены, если пользователь отходит от экрана, на котором выполняется приведенный выше код:

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

Что касается отмены, это правильный способ отменить токен, когда он используется в Задаче?

В частности, я проверил этот вопрос:

Использование свойства IsCancellationRequested?

и это заставляет меня думать, что я не делаю отмену правильным способом или возможно способом, который может вызвать исключение.

Кроме того, в этом случае после того, как я отменил, тогда я должен делать cts.Dispose ()?

alan2
источник
Обычно, используйте метод Cancel, чтобы передать запрос на отмену, а затем используйте метод Dispose, чтобы освободить память. Вы можете проверить образец в ссылке. docs.microsoft.com/en-us/dotnet/api/…
Венди Занг - MSFT

Ответы:

2

CancellationTokenSource.Cancel() является действительным способом начать отмену.

Опрос ct.IsCancellationRequestedизбегает бросков OperationCanceledException. Поскольку это опрос, он требует, чтобы итерация цикла завершилась до того, как он ответит на запрос отмены.

Если GetViewablePhrases()и CheckAvailability()можно изменить, чтобы принять a CancellationToken, это может сделать отмену быстрее, чтобы ответить, за счет OperationCanceledExceptionброска.

"Должен ли я делать cts.Dispose ()?" это не так просто ...

«Всегда утилизируйте IDisposables КАК МОЖНО СКОРЕЕ»

Это скорее руководство, чем правило. Taskсам по себе одноразовый, но вряд ли когда-либо напрямую расположен в коде.

Существуют случаи (когда WaitHandleиспользуются обработчики обратного вызова или отмены), когда утилизация ctsосвобождает ресурс / удаляет корень GC, который в противном случае был бы освобожден только финализатором. Они не относятся к вашему коду в том виде, в каком он есть, но могут и в будущем.

Добавление вызова Disposeпосле отмены гарантирует, что эти ресурсы будут быстро освобождены в будущих версиях кода.

Тем не менее, вам придется либо дождаться завершения кода, который использует, ctsпрежде чем вызывать dispose, либо изменить код, чтобы справляться с ObjectDisposedExceptionиспользованием cts(или его токена) после удаления.

Питер Вишарт
источник
«Подключите OnDisappearing для удаления cts». Это очень плохая идея, потому что она все еще используется внутри другой задачи. В частности, если позже кто-то изменит дизайн (измените подзадачи, чтобы принять CancellationTokenпараметр), вы можете избавиться от него, WaitHandleпока другой поток активно ожидает его :(
Ben Voigt
1
В частности, потому , что вы сделали заявление , что «отмена выполняет ту же очистку , как распоряжаются», было бы бессмысленно звонить Disposeиз OnDisappearing.
Бен Фойгт
Упс, я пропустил, что код в ответе уже называет Cancel...
Питер
Удалил заявление об отмене, выполнив ту же самую очистку (о которой я читал в другом месте), насколько я могу судить, единственная очистка Cancel- это внутренний таймер (если используется).
Питер
3

В целом, я вижу правильное использование токена отмены в вашем коде, но в соответствии с шаблоном асинхронного задания ваш код может быть отменен не сразу.

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

Для немедленного ответа код блокировки также следует отменить

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

Это зависит от вас, если вы должны утилизировать, если в прерванном коде зарезервировано много ресурсов памяти, вы должны это сделать.

Фидель Орозко
источник
1
И действительно, это также относится к вызову GetViewablePhrases - в идеале это будет также асинхронный вызов и в качестве опции будет принят токен отмены.
Пэдди
1

Я бы порекомендовал вам взглянуть на один из классов .net, чтобы полностью понять, как обрабатывать методы ожидания с CanncelationToken, я взял 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;
    }

Вы также можете просмотреть весь класс здесь, https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,6095d9030263f169

Muhab
источник