रद्द करने के लिए कब रद्द करें TokenSource?


163

वर्ग CancellationTokenSourceडिस्पोजेबल है। परावर्तक में एक त्वरित देखो KernelEventअप्रभावित संसाधन (, बहुत संभावना) के उपयोग को साबित करता है । चूंकि CancellationTokenSourceहमारा कोई फाइनल नहीं है, अगर हम इसका निपटान नहीं करते हैं, तो GC यह नहीं करेगा।

दूसरी ओर, यदि आप प्रबंधित थ्रेड्स में MSDN लेख रद्दीकरण पर सूचीबद्ध नमूनों को देखते हैं, तो केवल एक कोड स्निपेट टोकन का निपटान करता है।

कोड में इसका निपटान करने का उचित तरीका क्या है?

  1. usingयदि आप इसके लिए प्रतीक्षा नहीं करते हैं तो आप अपने समानांतर कार्य शुरू करने वाले कोड को लपेट नहीं सकते । और यह समझ में आता है कि यदि आप प्रतीक्षा नहीं करते हैं तो केवल रद्द करना है।
  2. बेशक आप ContinueWithएक Disposeकॉल के साथ कार्य को जोड़ सकते हैं , लेकिन क्या वह रास्ता है?
  3. रद्द करने योग्य PLINQ प्रश्नों के बारे में क्या है, जो वापस सिंक्रनाइज़ नहीं करते हैं, लेकिन अंत में कुछ करते हैं? हम कहते हैं.ForAll(x => Console.Write(x)) ?
  4. क्या यह पुन: प्रयोज्य है? क्या एक ही टोकन को कई कॉल के लिए इस्तेमाल किया जा सकता है और फिर इसे होस्ट घटक के साथ मिलकर डिस्पोज़ किया जा सकता है, आइए यूआई कंट्रोल कहते हैं?

क्योंकि इसमें Resetसाफ-सफाई IsCancelRequestedऔर Tokenक्षेत्र के लिए एक विधि जैसा कुछ नहीं है, तो मुझे लगता है कि यह पुन: प्रयोज्य नहीं होगा, इस प्रकार हर बार जब आप एक कार्य (या PLINQ क्वेरी) शुरू करते हैं तो आपको एक नया निर्माण करना चाहिए। क्या यह सच है? यदि हाँ, तो मेरा प्रश्न यह है कि Disposeउन कई CancellationTokenSourceउदाहरणों से निपटने के लिए सही और अनुशंसित रणनीति क्या है ?

जवाबों:


82

इस बारे में बोलते हुए कि क्या वास्तव में डिस्पोज़ को कॉल करना आवश्यक है CancellationTokenSource... मुझे अपने प्रोजेक्ट में एक मेमोरी लीक था और यह पता चला कि CancellationTokenSourceयह समस्या थी।

मेरी परियोजना की एक सेवा है, जो लगातार डेटाबेस पढ़ रही है और अलग-अलग कार्यों से दूर हो रही है, और मैं अपने कर्मचारियों से लिंक किए गए कैंसेलेशन टोकन पारित कर रहा था, इसलिए उनके पास प्रोसेसिंग डेटा समाप्त होने के बाद भी, कैंसलेशन टोकन निपटाए नहीं गए, जिससे मेमोरी लीक हो गई।

प्रबंधित थ्रेड्स में MSDN रद्दीकरण यह स्पष्ट रूप से बताता है:

ध्यान दें कि Disposeजब आप इसके साथ किए जाते हैं तो आपको लिंक किए गए टोकन स्रोत पर कॉल करना होगा । अधिक पूर्ण उदाहरण के लिए, देखें : कैसे करें: एकाधिक रद्दीकरण अनुरोधों के लिए सुनो

मैंने ContinueWithअपने कार्यान्वयन में उपयोग किया।


14
यह ब्रायन क्रॉस्बी द्वारा वर्तमान स्वीकृत उत्तर में एक महत्वपूर्ण चूक है - यदि आप एक लिंक सीटीएस बनाते हैं , तो आप मेमोरी लीक का जोखिम उठाते हैं। परिदृश्य बहुत हद तक ऐसे इवेंट हैंडलर के समान है जो कभी अपंजीकृत नहीं होते हैं।
सोरेन बोइसन

5
मेरे पास इसी मुद्दे के कारण लीक हुआ था। एक प्रोफाइलर का उपयोग करके मैं कॉलबैक पंजीकरण से जुड़े सीटीएस उदाहरणों के संदर्भ देख सकता था। सीटीएस निपटान कार्यान्वयन के लिए कोड की जांच यहां बहुत व्यावहारिक था और अंडरस्कोर @ SørenBoisen ईवेंट हैंडलर पंजीकरण लीक करने के लिए तुलना।
BitMask777

उपरोक्त टिप्पणियां चर्चा राज्य को दर्शाती हैं, @ ब्रायन क्रॉस्बी द्वारा अन्य उत्तर को स्वीकार किया गया था।
जॉर्ज ममलाडज़े

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

मुझे नहीं लगा कि कोई भी वर्तमान उत्तर संतोषजनक था। शोध के बाद मुझे स्टीफन टौब ( संदर्भ ) से यह उत्तर मिला :

निर्भर करता है। .NET 4 में, CTS.Dispose ने दो प्राथमिक उद्देश्य दिए। यदि रद्दीकरण टोकेन की वेटहैंडल को एक्सेस किया गया था (इस प्रकार आलसी इसे आवंटित करते हैं), डिस्पोजल उस हैंडल का निपटान करेगा। इसके अतिरिक्त, अगर CTS CreateLinkedTokenSource विधि के माध्यम से बनाया गया था, तो डिस्पैच CTS को उस टोकन से अनलिंक कर देगा, जिससे इसे जोड़ा गया था। .NET 4.5 में, Dispose का एक अतिरिक्त उद्देश्य है, जो कि अगर CTS कवर के तहत एक टाइमर का उपयोग करता है (उदाहरण के लिए CancelAfter को कॉल किया गया), टाइमर को Disposed किया जाएगा।

यह रद्द करने के लिए बहुत दुर्लभ है। इसका उपयोग किया जा सकता है, इसलिए इसे साफ करने के बाद आमतौर पर डिस्पोज का उपयोग करने का एक बड़ा कारण नहीं है। यदि, हालांकि, आप CreateLinkedTokenSource के साथ अपना सीटीएस बना रहे हैं, या यदि आप सीटीएस की टाइमर कार्यक्षमता का उपयोग कर रहे हैं, तो डिस्पोज़ का उपयोग करना अधिक प्रभावशाली हो सकता है।

मुझे लगता है कि बोल्ड हिस्सा महत्वपूर्ण हिस्सा है। वह "अधिक प्रभावशाली" का उपयोग करता है जो इसे थोड़ा अस्पष्ट छोड़ देता है। मैं इसकी व्याख्या कर रहा हूं, जिसका अर्थ है Disposeकि उन स्थितियों में कॉल किया जाना चाहिए, अन्यथा उपयोग करने Disposeकी आवश्यकता नहीं है।


10
अधिक प्रभावशाली का मतलब है कि बच्चे को सीटीएस माता-पिता में जोड़ा जाता है। यदि आप बच्चे का निपटान नहीं करते हैं तो माता-पिता के दीर्घजीवी होने पर रिसाव होगा। इसलिए लिंक किए गए लोगों को निपटाना महत्वपूर्ण है।
ग्रिगरी

26

मैंने ILSpy में एक नज़र डाली, CancellationTokenSourceलेकिन मैं केवल वही खोज सकता हूं m_KernelEventजो वास्तव में एक है ManualResetEvent, जो किसी WaitHandleवस्तु के लिए एक आवरण वर्ग है । इसे जीसी द्वारा ठीक से संभाला जाना चाहिए।


7
मुझे एक ही एहसास है कि जीसी सभी को साफ करेगा। मैं यह सत्यापित करने का प्रयास करूँगा। Microsoft ने इस मामले में निपटान क्यों लागू किया? इवेंट कॉलबैक से छुटकारा पाने के लिए और संभवतः दूसरी पीढ़ी जीसी के प्रचार से बचें। इस मामले में कॉलिंग डिस्पोज़ वैकल्पिक है - यदि आप इसे अनदेखा कर सकते हैं, तो इसे कॉल करें। मेरे विचार से सबसे अच्छा तरीका नहीं है।
जॉर्ज ममलाडज़े 19

4
मैंने इस मुद्दे की जांच की है। रद्दीकरण TokenSource कचरा एकत्र करता है। आप इसे GEN 1 GC में करने के लिए निपटान में मदद कर सकते हैं। स्वीकार किए जाते हैं।
जॉर्ज ममलाडज़े

1
मैंने यही जांच स्वतंत्र रूप से की और उसी नतीजे पर पहुंची: यदि आप आसानी से निपट सकते हैं, तो निपटाएं, लेकिन दुर्लभ-लेकिन-न-अनसुने मामलों में ऐसा करने की कोशिश में झल्लाहट न करें, जहां आपने एक कैंसलेशनेशन भेजा है। boondocks और उन्हें पोस्टकार्ड वापस लिखने के लिए उनके लिए इंतजार करने के लिए नहीं कह रहा है कि आप इसके साथ कर रहे हैं। यह अब हर बार होने जा रहा है और प्रकृति की वजह से कैंसेलेशनटोकन का उपयोग किया जाता है, और यह वास्तव में ठीक है, मैं वादा करता हूं।
जो Amenta

6
मेरी उपरोक्त टिप्पणी लिंक किए गए टोकन स्रोतों पर लागू नहीं होती है; मैं साबित नहीं कर सका कि इन अपरिहार्य को छोड़ना ठीक है, और इस धागे और MSDN में ज्ञान बताता है कि यह नहीं हो सकता है।
जो Amenta

23

आपको हमेशा निपटना चाहिए CancellationTokenSource

इसे कैसे निपटाना है यह वास्तव में परिदृश्य पर निर्भर करता है। आप कई अलग-अलग परिदृश्यों का प्रस्ताव करते हैं।

  1. using केवल तभी काम करता है जब आप उपयोग कर रहे हों CancellationTokenSource कुछ समानांतर काम होते हैं जिसका आप इंतजार कर रहे होते हैं। यदि वह आपका सेनेरिया है, तो बहुत अच्छा है, यह सबसे आसान तरीका है।

  2. कार्यों का उपयोग करते समय, का उपयोग करें ContinueWith कार्य का जैसा कि आपने निपटान करने का संकेत दिया था CancellationTokenSource

  3. प्लिंक के लिए आप उपयोग कर सकते हैं using इसे समानांतर में चला रहे हैं, लेकिन खत्म होने के लिए समानांतर चल रहे श्रमिकों के सभी पर इंतजार कर रहे बाद से आप ।

  4. UI के लिए, आप एक नया बना सकते हैं CancellationTokenSource प्रत्येक रद्द करने योग्य ऑपरेशन के लिए एक भी रद्द ट्रिगर से बंधा नहीं है। एक बनाए रखें List<IDisposable>और सूची में प्रत्येक स्रोत को जोड़ें, जब आपके घटक का निपटान किया जाता है, तो उन सभी को निपटाना।

  5. थ्रेड्स के लिए, एक नया थ्रेड बनाएँ, जो सभी वर्कर थ्रेड्स से जुड़ता है और वर्कर के थ्रेड्स समाप्त होने पर सिंगल सोर्स को बंद कर देता है। रद्दीकरण देखें

हमेशा एक रास्ता है। IDisposableउदाहरणों को हमेशा निपटाया जाना चाहिए। नमूने अक्सर नहीं होते हैं क्योंकि वे या तो मुख्य उपयोग दिखाने के लिए त्वरित नमूने होते हैं या क्योंकि प्रदर्शन किए जा रहे वर्ग के सभी पहलुओं को जोड़ने से नमूना के लिए अत्यधिक जटिल हो जाएगा। नमूना सिर्फ इतना है कि एक नमूना, आवश्यक नहीं (या आमतौर पर) उत्पादन गुणवत्ता कोड। सभी नमूने उत्पादन कोड में कॉपी किए जाने के लिए स्वीकार्य नहीं हैं।


बिंदु 2 के लिए, किसी भी कारण से आप awaitकार्य का उपयोग नहीं कर सके और इंतजार के बाद आने वाले कोड में रद्द करेंटोकेशन स्रोत का निपटान करें?
टिजिन

14
कैविएट हैं। यदि आप awaitऑपरेशन करते समय सीटीएस रद्द कर देते हैं , तो आप एक के कारण फिर से शुरू कर सकते हैं OperationCanceledException। आप तब कॉल कर सकते हैं Dispose()। लेकिन अगर ऑपरेशन अभी भी चल रहे हैं और इसी का उपयोग कर रहे हैं CancellationToken, कि टोकन अभी भी रिपोर्ट कर CanBeCanceledरहा है, trueभले ही स्रोत का निपटान किया गया हो। यदि वे रद्दीकरण कॉलबैक दर्ज करने का प्रयास करते हैं, BOOM! , ObjectDisposedException। यह Dispose()ऑपरेशन के सफल समापन के बाद कॉल करने के लिए पर्याप्त सुरक्षित है । यह वास्तव में मुश्किल हो जाता है जब आपको वास्तव में कुछ रद्द करने की आवश्यकता होती है।
माइक स्ट्रोबेल

8
माइक स्ट्रोबेल द्वारा दिए गए कारणों के लिए डाउनवोटेड - एक नियम को हमेशा कॉल करने के लिए मजबूर करना जब आप अपने अतुल्यकालिक स्वभाव के कारण सीटीएस और टास्क से निपटते हैं तो आप बालों वाली स्थितियों में पहुंच सकते हैं। नियम इसके बजाय होना चाहिए: हमेशा लिंक किए गए टोकन स्रोतों का निपटान करें ।
सोरेन बोइसन

1
आपका लिंक हटाए गए उत्तर पर जाता है।
त्रिशूल

19

यह उत्तर अभी भी Google खोजों में आ रहा है, और मेरा मानना ​​है कि मतदान किया गया उत्तर पूरी कहानी नहीं देता है। को देखने के बाद स्रोत कोड के लिए CancellationTokenSource(सीटीएस) और CancellationToken(सीटी) मुझे विश्वास है कि सबसे उपयोग के मामलों के लिए निम्न कोड अनुक्रम ठीक है:

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

m_kernelHandleआंतरिक ऊपर उल्लेख किया क्षेत्र का समर्थन कर तुल्यकालन वस्तु है WaitHandleदोनों सीटीएस और सीटी कक्षाओं में संपत्ति। यदि आप उस संपत्ति का उपयोग करते हैं तो यह केवल त्वरित है। इसलिए, जब तक आप WaitHandleअपने Taskकॉलिंग डिस्पोज़ में कुछ पुराने-स्कूल थ्रेड सिंक्रोनाइज़ेशन के लिए उपयोग कर रहे हैं, तब तक कोई प्रभाव नहीं पड़ेगा।

बेशक, अगर आप इसका उपयोग रहे हैं वही करना चाहिए जो ऊपर दिए गए अन्य उत्तरों द्वारा सुझाया गया है और Disposeजब तक WaitHandleकि हैंडल का उपयोग करने वाला कोई भी ऑपरेशन पूरा नहीं हो जाता, तब तक कॉल करने में देरी होती है , क्योंकि, जैसा कि WaitHandle के लिए Windows API प्रलेखन में वर्णित है , परिणाम अपरिभाषित हैं।


7
प्रबंधित थ्रेड्स में MSDN लेख रद्दीकरण बताता है: "श्रोता IsCancellationRequestedमतदान, कॉलबैक या प्रतीक्षा हैंडल द्वारा टोकन की संपत्ति के मूल्य की निगरानी करते हैं।" दूसरे शब्दों में: यह आप नहीं हो सकते हैं (यानी जो कि एसिंक्स अनुरोध कर रहा है) जो प्रतीक्षा हैंडल का उपयोग करता है, यह श्रोता (यानी अनुरोध का जवाब देने वाला) हो सकता है। जिसका अर्थ है कि, निपटान के लिए जिम्मेदार के रूप में, प्रभावी रूप से इस बात पर कोई नियंत्रण नहीं है कि प्रतीक्षा हैंडल का उपयोग किया जाता है या नहीं।
हर्जब्यूब

MSDN के अनुसार, पंजीकृत कॉलबैक जो कि अपवादित हैं, का कारण बनेंगे। आपका कोड कॉल नहीं करेगा। यदि ऐसा होता है। कॉलबैक को ऐसा न करने के लिए सावधान रहना चाहिए, लेकिन ऐसा हो सकता है।
जोसेफ लेनॉक्स

11

मुझे यह पूछे हुए एक लंबा समय हो गया है और कई सहायक उत्तर मिले हैं, लेकिन मुझे इस से संबंधित एक दिलचस्प मुद्दा सामने आया और मुझे लगा कि मैं इसे यहां एक और उत्तर के रूप में पोस्ट करूंगा:

आपको CancellationTokenSource.Dispose()केवल तभी कॉल करना चाहिए जब आप सुनिश्चित हों कि कोई भी सीटीएस की Tokenसंपत्ति प्राप्त करने की कोशिश करने वाला नहीं है । अन्यथा आपको इसे नहीं कहना चाहिए , क्योंकि यह एक दौड़ है। उदाहरण के लिए, यहां देखें:

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

इस मुद्दे को, कोड जो पहले था के लिए दुविधा में cts.Cancel(); cts.Dispose();बस को संपादित किया गया था करते cts.Cancel();क्योंकि बहुत अशुभ के रूप में रद्द आदेश के कारण इसे हटा राज्य निरीक्षण करने के लिए में टोकन प्राप्त करने की कोशिश करने के लिए किसी को भी बाद Dispose बुलाया गया है दुर्भाग्य से भी संभाल करने की आवश्यकता होगी ObjectDisposedException- के अलावा OperationCanceledExceptionकि वे योजना बना रहे थे।

इस निर्धारण से संबंधित एक अन्य महत्वपूर्ण अवलोकन ट्रचर द्वारा किया गया है: "निपटान केवल उन टोकन के लिए आवश्यक है जिन्हें रद्द नहीं किया जाएगा, क्योंकि रद्द करना सभी को एक ही सफाई देता है।" बस Cancel()निपटान के बजाय कर वास्तव में काफी अच्छा है!


1

मैंने एक थ्रेड-सुरक्षित वर्ग बनाया, जो एक CancellationTokenSourceको बांधता है 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 दूसरी बार करने से नया ऑपरेशन शुरू करने से पहले चुपचाप रद्द कर दिया जाएगा और पिछले ऑपरेशन के पूरा होने का इंतजार करेगा (यदि यह अभी भी चल रहा है)।

इस वर्ग का उपयोग किसी भी प्रकार के अनुप्रयोगों में किया जा सकता है। यूआई अनुप्रयोगों में इसका प्राथमिक उपयोग हालांकि, एक अतुल्यकालिक ऑपरेशन को शुरू करने और रद्द करने के लिए बटन के साथ रूपों में होता है, या एक सूची बॉक्स के साथ रद्द करता है और हर बार उसके चयनित आइटम को बदलने के लिए एक ऑपरेशन को रद्द और पुनरारंभ करता है। यहाँ पहले मामले का एक उदाहरण है:

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। इस वैकल्पिक टोकन की आपूर्ति अग्रिम परिदृश्यों में उपयोगी हो सकती है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.