เมื่อใดจึงจะยกเลิก CancellationTokenSource


163

ชั้นเรียนCancellationTokenSourceเป็นแบบใช้แล้วทิ้ง การดูอย่างรวดเร็วใน Reflector พิสูจน์การใช้งานKernelEventซึ่งเป็นทรัพยากรที่ไม่มีการจัดการ (มีโอกาสมาก) เนื่องจากCancellationTokenSourceไม่มี finalizer หากเราไม่ได้กำจัด GC จะไม่ทำเช่นนั้น

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

เป็นวิธีที่เหมาะสมในการกำจัดมันในรหัสอะไร

  1. คุณไม่สามารถตัดโค้ดเพื่อเริ่มงานคู่ขนานของคุณได้usingหากคุณไม่รอ และมันก็สมเหตุสมผลที่จะยกเลิกหากคุณไม่รอ
  2. แน่นอนว่าคุณสามารถเพิ่มContinueWithงานด้วยการDisposeโทร แต่นั่นเป็นวิธีที่จะไปไหม?
  3. แล้วแบบสอบถาม PLINQ ที่ยกเลิกได้ซึ่งไม่ซิงโครไนซ์ย้อนกลับ แต่ทำอะไรได้บ้างในตอนท้าย สมมติว่า.ForAll(x => Console.Write(x))อย่างไร
  4. มันสามารถใช้ซ้ำได้หรือไม่ สามารถใช้โทเค็นเดียวกันสำหรับการโทรหลายครั้งแล้วทิ้งพร้อมกับส่วนประกอบโฮสต์สมมติว่าการควบคุม UI ได้หรือไม่

เนื่องจากมันไม่มีResetวิธีการล้างข้อมูลIsCancelRequestedและTokenฟิลด์ฉันคิดว่ามันไม่สามารถใช้ซ้ำได้ดังนั้นทุกครั้งที่คุณเริ่มงาน (หรือแบบสอบถาม PLINQ) คุณควรสร้างใหม่ มันจริงหรอ? ถ้าใช่คำถามของฉันคือกลยุทธ์ที่ถูกต้องและแนะนำในการจัดการกับอินสแตนซ์Disposeเหล่านั้นCancellationTokenSourceคืออะไร?

คำตอบ:


82

พูดเกี่ยวกับว่าจำเป็นหรือไม่ที่จะต้องเรียกใช้การกำจัดCancellationTokenSource... ฉันมีหน่วยความจำรั่วในโครงการและปรากฏว่าCancellationTokenSourceเป็นปัญหา

โครงการของฉันมีบริการที่อ่านฐานข้อมูลอย่างต่อเนื่องและกำจัดงานที่แตกต่างกันและฉันส่งโทเค็นการยกเลิกการเชื่อมโยงไปยังคนงานของฉันดังนั้นแม้ว่าพวกเขาจะเสร็จสิ้นการประมวลผลข้อมูลโทเค็นการยกเลิกก็ไม่ได้ถูกกำจัด

การยกเลิก MSDN ในเธรดที่มีการจัดการระบุไว้อย่างชัดเจน:

โปรดสังเกตว่าคุณต้องโทรหาDisposeแหล่งโทเค็นที่เชื่อมโยงเมื่อคุณดำเนินการเสร็จแล้ว สำหรับตัวอย่างที่สมบูรณ์ยิ่งขึ้นดูที่วิธีการ: รับฟังคำขอยกเลิกหลายรายการ

ฉันใช้ContinueWithในการดำเนินการของฉัน


14
นี่คือการละเว้นที่สำคัญในคำตอบที่ยอมรับในปัจจุบันโดยไบรอันครอสบี - ถ้าคุณสร้างCTS ที่เชื่อมโยงคุณเสี่ยงต่อการรั่วไหลของหน่วยความจำ สถานการณ์คล้ายกับตัวจัดการเหตุการณ์ที่ไม่เคยลงทะเบียน
Søren Boisen

5
ฉันมีการรั่วไหลเนื่องจากปัญหาเดียวกันนี้ ใช้ profiler ฉันสามารถเห็นการลงทะเบียนการติดต่อกลับที่เก็บการอ้างอิงไปยังอินสแตนซ์ CTS ที่เชื่อมโยง การตรวจสอบรหัสสำหรับการดำเนินการกำจัด CTS ที่นี่นั้นมีความชาญฉลาดและขีดเส้นใต้ @ SørenBoisenเมื่อเปรียบเทียบกับการลงทะเบียนตัวจัดการเหตุการณ์
BitMask777

ความคิดเห็นข้างต้นสะท้อนให้เห็นถึงสถานะการสนทนาเป็นคำตอบอื่น ๆ โดย @Bryan Crosby ได้รับการยอมรับ
George Mamaladze

เอกสารในปี 2020 ชัดเจนว่า: Important: The CancellationTokenSource class implements the IDisposable interface. You should be sure to call the CancellationTokenSource.Dispose method when you have finished using the cancellation token source to free any unmanaged resources it holds.- docs.microsoft.com/en-us/dotnet/standard/threading/…
Endrju

44

ฉันไม่คิดว่าคำตอบใด ๆ ในปัจจุบันน่าพอใจ หลังจากการค้นคว้าฉันพบคำตอบนี้จาก Stephen Toub ( อ้างอิง ):

มันขึ้นอยู่กับ. ใน. NET 4 CTSDispose ให้บริการวัตถุประสงค์หลักสองประการ หากมีการเข้าถึง WaitHandle ของ CancellationToken (เช่นจัดสรรอย่างเกียจคร้าน) การจัดการจะกำจัดการจัดการดังกล่าว นอกจากนี้หาก CTS ถูกสร้างขึ้นผ่านวิธีการ CreateLinkedTokenSource การกำจัดจะยกเลิกการเชื่อมโยง CTS จากโทเค็นที่เชื่อมโยงกับ ใน. NET 4.5 การกำจัดมีวัตถุประสงค์เพิ่มเติมซึ่งหาก CTS ใช้ตัวจับเวลาภายใต้หน้าปก (เช่นชื่อ CancelAfter ถูกเรียก) ตัวจับเวลาจะถูกกำจัด

มันหายากมากสำหรับการยกเลิกเปิดใช้งานอย่าใช้มือจับดังนั้นการทำความสะอาดหลังจากนั้นโดยทั่วไปจะไม่มีเหตุผลที่ดีในการใช้การกำจัด อย่างไรก็ตามหากคุณกำลังสร้าง CTS ของคุณด้วย CreateLinkedTokenSource หรือถ้าคุณกำลังใช้ฟังก์ชันตัวจับเวลาของ CTS มันจะมีผลกระทบมากขึ้นในการใช้ Dispose

ส่วนที่ฉันคิดว่าเป็นตัวหนาเป็นส่วนสำคัญ เขาใช้ "มีประสิทธิภาพยิ่งขึ้น" ซึ่งทำให้มันค่อนข้างคลุมเครือ ฉันตีความว่าเป็นการเรียกความหมายDisposeในสถานการณ์เหล่านั้นควรทำมิฉะนั้นDisposeไม่จำเป็นต้องใช้


10
วิธีที่มีผลกระทบมากขึ้นคือการเพิ่ม CTS ลูกให้กับผู้ปกครอง หากคุณไม่ทิ้งเด็กจะมีการรั่วไหลหากผู้ปกครองมีอายุยืน ดังนั้นจึงเป็นเรื่องสำคัญอย่างยิ่งที่จะต้องกำจัดสิ่งที่เชื่อมโยง
กริกอ

26

ฉันดูใน ILSpy สำหรับCancellationTokenSourceแต่ฉันสามารถหาm_KernelEventที่เป็นจริงManualResetEventซึ่งเป็นชั้น wrapper สำหรับWaitHandleวัตถุ สิ่งนี้ควรได้รับการจัดการอย่างถูกต้องโดย GC


7
ฉันมีความรู้สึกเดียวกันกับที่ GC จะล้างข้อมูลทั้งหมด ฉันจะพยายามตรวจสอบว่า เหตุใด Microsoft จึงใช้งานการกำจัดในกรณีนี้ เพื่อกำจัดเหตุการณ์การเรียกกลับและหลีกเลี่ยงการแพร่กระจายไปยัง GC รุ่นที่สอง ในกรณีนี้การโทร Dispose เป็นทางเลือก - โทรถ้าคุณทำได้หากไม่สนใจ ไม่ใช่วิธีที่ดีที่สุดที่ฉันคิด
George Mamaladze

4
ฉันได้ตรวจสอบปัญหานี้แล้ว CancellationTokenSource ได้รับการรวบรวมขยะ คุณอาจช่วยจัดการทิ้งใน GEN 1 GC ได้รับการยอมรับ
George Mamaladze

1
ฉันได้ทำการสอบสวนแบบเดียวกันนี้อย่างเป็นอิสระและมาถึงข้อสรุปเดียวกัน: กำจัดถ้าคุณทำได้ แต่ไม่ต้องกังวลใจในการพยายามทำเช่นนั้นในกรณีที่หายาก แต่ไม่เคยได้ยินที่คุณส่ง CancellationToken boondocks และไม่ต้องการรอให้พวกเขาเขียนโปสการ์ดกลับบอกว่าคุณทำเสร็จแล้ว สิ่งนี้จะเกิดขึ้นทุก ๆ คราวเพราะธรรมชาติของสิ่งที่ CancellationToken ใช้สำหรับและมันก็โอเคฉันสัญญา
Joe Amenta

6
ความคิดเห็นด้านบนของฉันใช้ไม่ได้กับแหล่งโทเค็นที่เชื่อมโยง ฉันไม่สามารถพิสูจน์ได้ว่ามันเป็นเรื่องปกติที่จะปล่อยสิ่งที่ไม่ได้ใช้เหล่านี้ออกไปและภูมิปัญญาในหัวข้อนี้และ MSDN แสดงให้เห็นว่ามันอาจจะไม่ใช่
Joe Amenta

23

CancellationTokenSourceคุณควรทิ้ง

วิธีกำจัดมันขึ้นอยู่กับสถานการณ์ คุณเสนอสถานการณ์ที่แตกต่างกัน

  1. usingใช้ได้เฉพาะเมื่อคุณใช้CancellationTokenSourceงานแบบขนานบางอย่างที่คุณรออยู่ หากนั่นคือ senario ของคุณก็ยอดเยี่ยมนั่นเป็นวิธีที่ง่ายที่สุด

  2. เมื่อมีการใช้งานใช้งานที่คุณระบุไว้ในการกำจัดของContinueWithCancellationTokenSource

  3. สำหรับ plinq คุณสามารถใช้งานได้usingเนื่องจากคุณใช้งานแบบขนาน แต่รอให้พนักงานที่ทำงานแบบขนานทั้งหมดเสร็จสิ้น

  4. สำหรับ UI คุณสามารถสร้างใหม่CancellationTokenSourceสำหรับแต่ละการดำเนินการที่ยกเลิกได้ซึ่งไม่ผูกกับทริกเกอร์การยกเลิกเดียว รักษาList<IDisposable>และเพิ่มแหล่งที่มาแต่ละรายการลงในรายการโดยกำจัดแหล่งข้อมูลทั้งหมดเมื่อส่วนประกอบของคุณถูกกำจัด

  5. สำหรับเธรดสร้างเธรดใหม่ที่รวมเธรดผู้ปฏิบัติงานทั้งหมดและปิดแหล่งเดียวเมื่อเธรดผู้ปฏิบัติงานทั้งหมดเสร็จสิ้น ดูการยกเลิกโทเคนแหล่งที่มาเมื่อไรจะจัดการ?

มีวิธีเสมอ IDisposableอินสแตนซ์ควรถูกกำจัดเสมอ ตัวอย่างมักจะไม่เป็นเพราะพวกมันเป็นตัวอย่างที่รวดเร็วในการแสดงการใช้งานหลักหรือเนื่องจากการเพิ่มในทุกด้านของคลาสที่แสดงให้เห็นนั้นซับซ้อนเกินไปสำหรับตัวอย่าง ตัวอย่างเป็นเพียงตัวอย่างที่ไม่จำเป็นต้อง (หรือแม้กระทั่งปกติ) รหัสคุณภาพการผลิต ตัวอย่างทั้งหมดไม่สามารถคัดลอกลงในรหัสการผลิตได้


สำหรับจุดที่ 2 เหตุผลใดที่คุณไม่สามารถใช้awaitกับงานและกำจัด CancellationTokenSource ในรหัสที่มาหลังจากรอคอย?
stijn

14
มีข้อแม้ หาก CTS ได้รับการยกเลิกในขณะที่คุณดำเนินการคุณอาจดำเนินการต่อเนื่องมาจากการawait จากนั้นคุณอาจจะเรียกOperationCanceledException Dispose()แต่ถ้ามีการดำเนินการยังคงทำงานอยู่และใช้สิ่งที่เกี่ยวข้องCancellationTokenโทเค็นนั้นยังคงรายงานCanBeCanceledว่ามีอยู่trueแม้ว่าแหล่งจะถูกกำจัด หากพวกเขาพยายามที่จะลงทะเบียนการโทรกลับการยกเลิกBOOM! , ObjectDisposedException. มันปลอดภัยพอที่จะโทรหาDispose()หลังจากการดำเนินการเสร็จสมบูรณ์ มันจะยุ่งยากมากเมื่อคุณต้องการยกเลิกบางสิ่งบางอย่าง
Mike Strobel

8
Downvoted ด้วยเหตุผลที่ Mike Strobel กำหนดไว้ - การบังคับใช้กฎเพื่อเรียก Dispose สามารถทำให้คุณตกอยู่ในสถานการณ์ที่ลำบากเมื่อต้องรับมือกับ CTS และ Task เนื่องจากธรรมชาติของมันไม่ตรงกัน กฎควรเป็น: กำจัดแหล่งโทเค็นที่เชื่อมโยงเสมอ
Søren Boisen

1
ลิงก์ของคุณไปที่คำตอบที่ถูกลบ
ริป

19

คำตอบนี้ยังคงเกิดขึ้นในการค้นหาของ Google และฉันเชื่อว่าคำตอบที่โหวตแล้วไม่ได้ให้เรื่องราวทั้งหมด หลังจากดูซอร์สโค้ดสำหรับCancellationTokenSource(CTS) และCancellationToken(CT) ฉันเชื่อว่าสำหรับกรณีการใช้งานส่วนใหญ่ลำดับของรหัสต่อไปนี้จะใช้ได้:

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}

m_kernelHandleข้อมูลภายในดังกล่าวข้างต้นเป็นวัตถุประสานสำรองWaitHandleสถานที่ให้บริการทั้งในและ CTS CT ชั้นเรียน มันเป็นอินสแตนซ์เท่านั้นถ้าคุณเข้าถึงคุณสมบัติ ดังนั้นหากคุณไม่ได้ใช้WaitHandleการซิงโครไนซ์เธรดโรงเรียนเก่าในการTaskจัดการการโทรของคุณจะไม่มีผลใด ๆ

แน่นอนหากคุณกำลังใช้งานอยู่คุณควรทำสิ่งที่แนะนำโดยคำตอบอื่น ๆ ข้างต้นและการโทรล่าช้าDisposeจนกว่าWaitHandleการดำเนินการใด ๆ ที่ใช้หมายเลขอ้างอิงจะเสร็จสมบูรณ์เนื่องจากตามที่อธิบายไว้ในเอกสารประกอบ Windows API สำหรับ WaitHandleผลลัพธ์จะไม่ได้กำหนด


7
บทความการยกเลิก MSDN ในสถานะของเธรดที่มีการจัดการ : "Listeners ตรวจสอบค่าของIsCancellationRequestedคุณสมบัติของโทเค็นโดยการโพลโทรกลับหรือรอจัดการ" ในคำอื่น ๆ : มันอาจจะไม่ใช่คุณ (เช่นคนที่ทำคำขอ async) ที่ใช้มือจับรอมันอาจจะเป็นผู้ฟัง (เช่นคนที่ตอบคำขอ) ซึ่งหมายความว่าคุณในฐานะผู้รับผิดชอบในการจัดการไม่มีประสิทธิภาพในการควบคุมว่าจะใช้การจัดการการรอหรือไม่
herzbube

ตาม MSDN การเรียกกลับที่ลงทะเบียนแล้วซึ่งได้รับการยกเว้นจะทำให้เกิดการยกเลิก รหัสของคุณจะไม่โทร. ทิ้ง () หากเกิดเหตุการณ์นี้ การเรียกกลับควรระวังไม่ให้ทำเช่นนี้ แต่สามารถเกิดขึ้นได้
โจเซฟเลนน็อกซ์

11

เป็นเวลานานแล้วที่ฉันถามคำถามนี้และได้รับคำตอบที่เป็นประโยชน์มากมาย แต่ฉันเจอปัญหาที่น่าสนใจเกี่ยวกับเรื่องนี้และคิดว่าฉันจะโพสต์ที่นี่เป็นคำตอบที่แปลกใหม่:

คุณควรโทรCancellationTokenSource.Dispose()เฉพาะเมื่อคุณแน่ใจว่าไม่มีใครพยายามรับTokenทรัพย์สินของ CTS มิฉะนั้นคุณไม่ควรเรียกมันเพราะมันเป็นการแข่งขัน ตัวอย่างเช่นดูที่นี่:

https://github.com/aspnet/AspNetKatana/issues/108

ในการแก้ไขปัญหานี้รหัสที่เคยทำก่อนหน้านี้cts.Cancel(); cts.Dispose();ถูกแก้ไขเพียงcts.Cancel();เพราะทุกคนโชคไม่ดีที่พยายามรับโทเค็นการยกเลิกเพื่อสังเกตสถานะการยกเลิกหลังจาก Disposeถูกเรียกว่าน่าเสียดายที่ต้องจัดการObjectDisposedException- นอกเหนือจากOperationCanceledExceptionที่พวกเขาวางแผนไว้

การสังเกตคีย์อื่น ๆ ที่เกี่ยวข้องกับการแก้ไขนี้ทำโดย Tratcher: "การกำจัดจำเป็นสำหรับโทเค็นที่จะไม่ถูกยกเลิกเท่านั้นเนื่องจากการยกเลิกจะเป็นการล้างข้อมูลเดียวกันทั้งหมด" นั่นคือเพียงแค่ทำCancel()แทนการกำจัดดีพอจริง ๆ !


1

ฉันสร้างคลาส thread-safe ที่ผูก a CancellationTokenSourceถึง a Taskและรับประกันว่าCancellationTokenSourceจะถูกกำจัดเมื่อความเกี่ยวข้องTaskเสร็จสมบูรณ์ มันใช้ล็อคเพื่อให้แน่ใจว่าCancellationTokenSourceจะไม่ถูกยกเลิกในระหว่างหรือหลังจากมันถูกกำจัด สิ่งนี้เกิดขึ้นเพื่อให้สอดคล้องกับเอกสารที่ระบุว่า:

Disposeวิธีการจะต้องใช้เฉพาะเมื่อมีการดำเนินการอื่น ๆ ทั้งหมดเกี่ยวกับCancellationTokenSourceวัตถุได้เสร็จสิ้น

และยัง :

DisposeวิธีใบCancellationTokenSourceอยู่ในสภาพที่ใช้งานไม่ได้

นี่คือคลาส:

public class CancelableExecution
{
    private readonly bool _allowConcurrency;
    private Operation _activeOperation;

    private class Operation : IDisposable
    {
        private readonly object _locker = new object();
        private readonly CancellationTokenSource _cts;
        private readonly TaskCompletionSource<bool> _completionSource;
        private bool _disposed;

        public Task Completion => _completionSource.Task; // Never fails

        public Operation(CancellationTokenSource cts)
        {
            _cts = cts;
            _completionSource = new TaskCompletionSource<bool>(
                TaskCreationOptions.RunContinuationsAsynchronously);
        }
        public void Cancel()
        {
            lock (_locker) if (!_disposed) _cts.Cancel();
        }
        void IDisposable.Dispose() // Is called only once
        {
            try
            {
                lock (_locker) { _cts.Dispose(); _disposed = true; }
            }
            finally { _completionSource.SetResult(true); }
        }
    }

    public CancelableExecution(bool allowConcurrency)
    {
        _allowConcurrency = allowConcurrency;
    }
    public CancelableExecution() : this(false) { }

    public bool IsRunning =>
        Interlocked.CompareExchange(ref _activeOperation, null, null) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> taskFactory,
        CancellationToken extraToken = default)
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken, default);
        using (var operation = new Operation(cts))
        {
            // Set this as the active operation
            var oldOperation = Interlocked.Exchange(ref _activeOperation, operation);
            try
            {
                if (oldOperation != null && !_allowConcurrency)
                {
                    oldOperation.Cancel();
                    await oldOperation.Completion; // Continue on captured context
                }
                var task = taskFactory(cts.Token); // Run in the initial context
                return await task.ConfigureAwait(false);
            }
            finally
            {
                // If this is still the active operation, set it back to null
                Interlocked.CompareExchange(ref _activeOperation, null, operation);
            }
        }
    }

    public Task RunAsync(Func<CancellationToken, Task> taskFactory,
        CancellationToken extraToken = default)
    {
        return RunAsync<object>(async ct =>
        {
            await taskFactory(ct).ConfigureAwait(false);
            return null;
        }, extraToken);
    }

    public Task CancelAsync()
    {
        var operation = Interlocked.CompareExchange(ref _activeOperation, null, null);
        if (operation == null) return Task.CompletedTask;
        operation.Cancel();
        return operation.Completion;
    }

    public bool Cancel() => CancelAsync() != Task.CompletedTask;
}

วิธีการหลักของCancelableExecutionชั้นเป็นและRunAsync Cancelตามค่าเริ่มต้นไม่อนุญาตให้มีการดำเนินการพร้อมกันหมายความว่าการโทรRunAsyncเป็นครั้งที่สองจะยกเลิกและรอการดำเนินการก่อนหน้า (ในกรณีที่ยังทำงานอยู่) ก่อนที่จะเริ่มการดำเนินการใหม่

คลาสนี้สามารถใช้ได้กับแอพพลิเคชั่นทุกชนิด แม้ว่าการใช้งานหลักจะอยู่ในแอปพลิเคชัน UI ภายในฟอร์มที่มีปุ่มสำหรับเริ่มต้นและยกเลิกการทำงานแบบอะซิงโครนัสหรือมีกล่องรายการที่ยกเลิกและรีสตาร์ทการดำเนินการทุกครั้งที่มีการเปลี่ยนแปลงรายการที่เลือก นี่คือตัวอย่างของกรณีแรก:

private readonly CancelableExecution _cancelableExecution = new CancelableExecution();

private async void btnExecute_Click(object sender, EventArgs e)
{
    string result;
    try
    {
        Cursor = Cursors.WaitCursor;
        btnExecute.Enabled = false;
        btnCancel.Enabled = true;
        result = await _cancelableExecution.RunAsync(async ct =>
        {
            await Task.Delay(3000, ct); // Simulate some cancelable I/O operation
            return "Hello!";
        });
    }
    catch (OperationCanceledException)
    {
        return;
    }
    finally
    {
        btnExecute.Enabled = true;
        btnCancel.Enabled = false;
        Cursor = Cursors.Default;
    }
    this.Text += result;
}

private void btnCancel_Click(object sender, EventArgs e)
{
    _cancelableExecution.Cancel();
}

RunAsyncวิธีการยอมรับเพิ่มเป็นอาร์กิวเมนต์ที่เชื่อมโยงกับที่สร้างขึ้นภายในCancellationToken CancellationTokenSourceการจัดหาโทเค็นตัวเลือกนี้อาจมีประโยชน์ในสถานการณ์ที่ก้าวหน้า

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