วิธีที่ถูกต้องในการยกเลิกโทเค็นการยกเลิกถูกใช้ในงานหรือไม่?


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 () หรือไม่


โดยปกติใช้วิธีการยกเลิกเพื่อสื่อสารการร้องขอการยกเลิกและจากนั้นใช้วิธีการกำจัดเพื่อปล่อยหน่วยความจำ คุณสามารถตรวจสอบตัวอย่างในลิงค์ docs.microsoft.com/en-us/dotnet/api/…
เวนดี้

คำตอบ:


2

CancellationTokenSource.Cancel() เป็นวิธีที่ถูกต้องในการเริ่มการยกเลิก

Polling หลีกเลี่ยงการขว้างปาct.IsCancellationRequested OperationCanceledExceptionเนื่องจากการสำรวจความคิดเห็นมันต้องการการวนซ้ำเพื่อให้สมบูรณ์ก่อนที่จะตอบคำขอยกเลิก

หากGetViewablePhrases()และCheckAvailability()สามารถแก้ไขได้เพื่อยอมรับ a สิ่งCancellationTokenนี้อาจทำให้การยกเลิกเร็วขึ้นเพื่อตอบสนองโดยมีค่าใช้จ่ายในการOperationCanceledExceptionโยน

"ฉันควรทำ cts.Dispose ()?" ไม่ตรงไปตรงมาว่า ...

"กำจัด IDisposables โดยเร็วที่สุดเสมอ"

เป็นแนวทางมากกว่ากฎ Taskตัวเองเป็นแบบใช้แล้วทิ้ง แต่ก็ไม่เคยทิ้งในโค้ดโดยตรง

มีหลายกรณี (เมื่อWaitHandleหรือมีการใช้ตัวจัดการการเรียกกลับที่ยกเลิก) ซึ่งการกำจัดctsจะทำให้ทรัพยากร / ลบราก GC ซึ่งมิฉะนั้นจะเป็นอิสระจาก Finalizer เท่านั้น สิ่งเหล่านี้ไม่สามารถใช้ได้กับรหัสของคุณตามที่คาด แต่อาจเกิดขึ้นในอนาคต

การเพิ่มการเรียกไปDisposeหลังจากยกเลิกจะรับประกันได้ว่าทรัพยากรเหล่านี้จะถูกปล่อยอย่างอิสระในรหัสรุ่นอนาคต

อย่างไรก็ตามคุณจะต้องรอให้รหัสที่ใช้ctsเสร็จก่อนโทรออกหรือแก้ไขรหัสเพื่อจัดการObjectDisposedExceptionจากการใช้cts(หรือโทเค็น) หลังจากการกำจัด


"ขอให้หายไปเพื่อกำจัด cts" ดูเหมือนความคิดที่แย่มากเพราะมันยังคงถูกใช้ในงานอื่นอยู่ โดยเฉพาะอย่างยิ่งหากมีคนเปลี่ยนการออกแบบในภายหลัง (แก้ไขงานย่อยเพื่อรับCancellationTokenพารามิเตอร์) คุณสามารถทิ้งWaitHandleขณะที่เธรดอื่นกำลังรอมันอยู่ :(
Ben Voigt

1
โดยเฉพาะอย่างยิ่งเพราะคุณทำอ้างว่า "ยกเลิกการดำเนินการทำความสะอาดเช่นเดียวกับทิ้ง" มันจะไม่มีจุดหมายที่จะเรียกจากDispose OnDisappearing
Ben Voigt

อ๊ะฉันคิดถึงรหัสในคำตอบที่ได้โทรไปแล้วCancel...
Peter Wishart

ได้ลบการอ้างสิทธิ์เกี่ยวกับการยกเลิกการทำล้างข้อมูลเดียวกัน (ซึ่งฉันได้อ่านที่อื่น) ดังนั้นเท่าที่ฉันสามารถบอกได้ว่าการล้างข้อมูลเพียงอย่างเดียวCancelคือตัวจับเวลาภายใน (ถ้าใช้)
Peter Wishart

3

โดยทั่วไปฉันเห็นว่ามีการใช้งานการยกเลิกโทเค็นอย่างเป็นธรรมในรหัสของคุณ แต่ตามรูปแบบงาน Async รหัสของคุณอาจไม่สามารถยกเลิกได้ทันที

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 ด้วย - เป็นการดีที่นี่จะเป็นการโทรแบบ async ด้วยและใช้โทเค็นการยกเลิกเป็นตัวเลือก
Paddy

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.