取消任务的取消令牌的正确方法是什么?

11

我有一段创建取消标记的代码

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

关于取消,当token在任务中使用时,这是否是正确的取消方式?

我特别查看了这个问题:

使用IsCancellationRequested属性?

这让我觉得我可能没有正确地取消,或者以一种可能导致异常的方式取消。

此外,在这种情况下,我取消后是否应该执行cts.Dispose()?


通常情况下,使用Cancel方法来发送取消请求,然后使用Dispose方法释放内存。您可以在以下链接中查看示例:https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.cancel?view=netframework-4.8 - Wendy Zang - MSFT
3个回答

4
一般来说,我看到你的代码中使用了合理的取消令牌(Cancel Token),但根据任务异步模式(Task Async Pattern)的要求,你的代码可能无法立即取消。
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的调用 - 理想情况下,这也应该是一个异步调用,并提供取消令牌作为选项。 - Paddy

2
CancellationTokenSource.Cancel() 是启动取消操作的有效方法。
轮询 ct.IsCancellationRequested 可以避免抛出 OperationCanceledException。但是,由于它是轮询的,需要完成循环迭代后才能响应取消请求。
如果可以修改 GetViewablePhrases()CheckAvailability() 以接受 CancellationToken,这可能会使取消更快地响应,但代价是会抛出 OperationCanceledException
“我是否应该执行 cts.Dispose()?”并不那么简单...

“尽快处理 IDisposables”

这只是一个指南而非规则。 Task 本身是可释放的,但在代码中几乎从未直接释放。
有些情况下(当使用 WaitHandle 或取消回调处理程序时),释放 cts 将释放资源 / 删除 GC 根,否则仅会由终结器释放。这些情况不适用于当前的代码,但将来可能会发生。
在取消后添加调用 Dispose 将确保在将来版本的代码中及时释放这些资源。
但是,您必须等待使用 cts 的代码完成后才能调用 dispose,或者修改代码以处理在处理程序或其标记释放后使用 cts 时引发的 ObjectDisposedException

将OnDisappearing挂钩到dispose cts似乎是一个非常糟糕的想法,因为它仍然在另一个任务中使用。特别是如果有人稍后更改设计(修改子任务以接受CancellationToken参数),您可能会在另一个线程正在积极等待它时处置WaitHandle :( - Ben Voigt
1
特别是,因为您声称“取消执行与处理相同的清理操作”,所以从OnDisappearing调用Dispose将毫无意义。 - Ben Voigt
糟糕,我错过了答案中已经调用“Cancel”的代码... - Peter Wishart
已删除有关取消执行相同清理的声明(我在 Stack Overflow 上读到过),据我所知,Cancel 执行的唯一清理是内部计时器(如果使用)。 - Peter Wishart

1
我建议你查看其中一个 .net 类,以完全了解如何使用 CancellationToken 处理等待方法,我选择了 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接