.NET में Async प्रोग्रामिंग: सर्वोत्तम अभ्यास

C # में async / प्रतीक्षा का आगमन सरल और सही समानांतर कोड लिखने के तरीके को पुनर्परिभाषित करने के लिए प्रेरित करता है। अक्सर, अतुल्यकालिक प्रोग्रामिंग का उपयोग करते हुए, प्रोग्रामर न केवल उन समस्याओं को हल करते हैं जो थ्रेड्स के साथ थे, बल्कि नए लोगों को भी पेश करते हैं। गतिरोध और उड़ानें कहीं भी नहीं जाती हैं - वे निदान करना कठिन हो जाता है।



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

कटस्कैन के तहत - डोटनेक्स्ट 2019 पाइटर सम्मेलन से दिमित्री की रिपोर्ट की वीडियो रिकॉर्डिंग और टेक्स्ट ट्रांसक्रिप्ट।



वक्ता की ओर से आगे का कथन।

बहु-थ्रेडेड या एसिंक्रोनस कोड में, कुछ अक्सर टूट जाता है। इसका कारण गतिरोध और दौड़ दोनों हो सकता है। एक नियम के रूप में, एक दौड़ एक हजार में से एक बार दुर्घटनाग्रस्त होती है, अक्सर स्थानीय रूप से नहीं, केवल एक बिल्ड सर्वर पर, और इसे पकड़ने में कई दिन लगते हैं। मुझे यकीन है कि कई लोगों के लिए यह एक परिचित स्थिति है।

इसके अलावा, अनुभवी डेवलपर्स द्वारा भी अतुल्यकालिक कोड को देखते हुए, मैं खुद को यह सोचकर पाता हूं कि कुछ चीजों को तीन गुना कम और अधिक सही तरीके से लिखा जा सकता है।

यह बताता है कि समस्या लोगों में नहीं है, लेकिन साधन में है। लोग सिर्फ उपकरण का उपयोग करते हैं और चाहते हैं कि यह उनकी समस्या का समाधान हो। इस टूल में बहुत बड़ी संख्या में क्षमताएं (कभी-कभी अति सूक्ष्म भी हैं), सेटिंग्स, एक अंतर्निहित संदर्भ, जो इस तथ्य की ओर जाता है कि गलत तरीके से उपयोग करना बहुत आसान है। आइए यह पता लगाने की कोशिश करें कि कैसे async का उपयोग करें / प्रतीक्षा करें और Task.NET में एक वर्ग के साथ काम करें

योजना


  • एस्कॉन / वेट के साथ हल होने वाले एप्रोच की समस्याएं।
  • विवादास्पद डिजाइन के उदाहरण।
  • वास्तविक जीवन का एक कार्य जिसे हम अतुल्यकालिक रूप से हल करेंगे।


Async / प्रतीक्षा और समस्याओं को हल करने के लिए




हमें async / प्रतीक्षा की आवश्यकता क्यों है? मान लीजिए कि हमारे पास कोड है जो साझा की गई मेमोरी के साथ काम करता है।

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

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

अब बात करते हैं दूसरे टास्क की। फ़ाइल को पढ़ने के बाद, इसे किसी तरह संसाधित करने की आवश्यकता है। हम इसे समानांतर में करेंगे।

आप में से कई लोगों ने सुना है कि समानता अतुल्यकालिक के समान नहीं है। इस मामले में, सवाल उठता है: क्या एसिंक्रोनस समानांतर कोड को अधिक कॉम्पैक्ट, सुंदर और तेज लिखने में मदद कर सकता है?

अंतिम कार्य साझा मेमोरी के साथ काम करना है। क्या हमें इस तंत्र को ताले, सिंक्रोनाइज़ेशन से एसिंक्रोनस कोड के साथ खींचने की आवश्यकता है, या क्या इससे किसी तरह बचा जा सकता है? क्या async / इसके साथ मदद का इंतजार कर सकता है ?

Async / प्रतीक्षा का पथ


आइए दुनिया में सामान्य रूप से और .NET में असिंक्रोनस प्रोग्रामिंग के विकास को देखें।

वापस कॉल करें


Void Foo(params, Action callback) {…}
 

Void OurMethod() {//synchronous code
 
    Foo(params,() =>{//asynchronous code;continuation
    });
}

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

अधिक कॉलबैक


void Foo(params, Action callback) {...} 
void Bar(Action callback) {...}
void Baz(Action callback) {...}

void OurMethod() {
    ... //synchronous code
    
    Foo(params, () => { 
      ... //continuation 1 
      Bar(() => {
        //continuation 2
        Baz(() => {
          //continuation 3
        }); 
      });
    });
}

इस प्रकार, एक कॉलबैक से आप एक और कॉलबैक रजिस्टर कर सकते हैं , जिसमें से आप तीसरे कॉलबैक को पंजीकृत कर सकते हैं, और अंत में यह सब कॉलबैक हेल में बदल जाता है



कॉलबैक: अपवाद



void Foo(params, Action onSuccess, Action onFailure) {...}


void OurMethod() {
    ... //synchronous code 
    Foo(params, () => {
      ... //asynchronous code on success 
    },
    () => {
        ... //asynchronous code on failure
    }); 
}

अपवादों के साथ कैसे काम करें? उदाहरण के लिए, ReSharper, जब अलग-अलग अपवादों और अच्छे निष्पादन के लिए प्रतिक्रिया करता है, तो कोड के सबसे सुंदर टुकड़ों को प्रदर्शित नहीं करता है - एक असाधारण स्थिति के लिए और एक सफल निरंतरता के लिए अलग-अलग कॉलबैक हैं। परिणाम सिर्फ एक ऐसी कॉलबैक नरक है , लेकिन रैखिक नहीं है, लेकिन पेड़ की तरह है, जो पूरी तरह से भ्रमित हो सकता है।



.NET में, पहले कॉलबैक दृष्टिकोण को एसिंक्रोनस प्रोग्रामिंग मॉडल (APM) कहा जाता है । विधि को बुलाया जाएगा AsyncCallback, जो अनिवार्य रूप से समान है Action, लेकिन दृष्टिकोण में कुछ विशेषताएं हैं। सबसे पहले, तरीकों को "आरंभ" शब्द से शुरू करना चाहिए (एक फ़ाइल से पढ़ना बिग्रेड है), जो कुछ रिटर्न देता है AsyncResult। स्वयंAsyncResult- यह एक हैंडलर है जो जानता है कि ऑपरेशन पूरा हो गया है और उसके पास एक तंत्र है WaitHandle। आप WaitHandleअसंगत रूप से पूर्ण होने के लिए ऑपरेशन की प्रतीक्षा कर सकते हैं। दूसरी ओर, आप कॉल कर सकते हैं EndOperation, EndReadअर्थात् , बना सकते हैं और समान रूप से लटका सकते हैं (जो एक संपत्ति के समान है Task.Result)।

इस दृष्टिकोण में कई समस्याएं हैं। सबसे पहले, यह कॉलबैक नरक से हमारी रक्षा नहीं करता है । दूसरे, यह पूरी तरह से स्पष्ट नहीं है कि अपवादों का क्या करना है। तीसरा, यह स्पष्ट नहीं है कि यह कॉलबैक किस थ्रेड पर कहा जाएगा - कॉल पर हमारा कोई नियंत्रण नहीं है। चौथा, सवाल उठता है कि कॉलबैक के साथ कोड के टुकड़ों को कैसे मिलाएं?



दूसरे मॉडल को इवेंट-आधारित एसिंक्रोनस पैटर्न कहा जाता है। यह एक प्रतिक्रियाशील कॉलबैक दृष्टिकोण है। विधि का विचार यह है कि हम उस विधि के पास जाते हैं जिसमें OperationNameAsyncकोई वस्तु होती है, जो इस घटना को पूरा करती है और इस घटना की सदस्यता लेती है। जैसा कि आपने देखा, में BeginOperationNameपरिवर्तन OperationNameAsync। सॉकेट क्लास में जाने पर कन्फ्यूजन हो सकता है, जहां दो पैटर्न मिक्स होते हैं: ConnectAsyncऔर BeginConnect

कृपया ध्यान दें कि आपको रद्द करने के लिए कॉल करना होगा OperationNameAsyncCancel। .NET में चूंकि यह कहीं और नहीं पाया जाता है, आमतौर पर हर कोई रद्दोबदल करता है । इस प्रकार, यदि आप गलती से पुस्तकालय में एक विधि का सामना करते हैं जो समाप्त हो जाता है Async, तो आपको यह समझने की आवश्यकता है कि यह जरूरी नहीं है Task, लेकिन एक समान निर्माण वापस कर सकता है।



एक मॉडल पर विचार करें जो जावा में जाना जाता हैवायदा , जावास्क्रिप्ट में, वादे के रूप में , और .NET में, टास्क अतुल्यकालिक पैटर्न के रूप में, दूसरे शब्दों में, "कार्य"। यह विधि मानती है कि आपके पास कुछ गणना ऑब्जेक्ट है, और आप इस ऑब्जेक्ट की स्थिति (चल या समाप्त) देख सकते हैं। .NET में, RnToCompletionदो स्थितियों की एक तथाकथित , सुविधाजनक जुदाई है: कार्य की शुरुआत और कार्य पूरा करना। एक सामान्य त्रुटि तब होती है जब किसी कार्य पर एक विधि को कॉल किया जाता है जो IsCompletedसफल निरंतरता नहीं देता है, लेकिन RnToCompletion, Canceledऔर Faulted। इस प्रकार, UI एप्लिकेशन में "रद्द करें" पर क्लिक करने का परिणाम अपवादों (निष्पादन) की वापसी से भिन्न होना चाहिए। .NET में, एक भेद किया गया है: यदि निष्पादन आपकी गलती है जिसे आप सुरक्षित करना चाहते हैं, तो रद्द करें- मजबूर ऑपरेशन।

.NET में, एक अवधारणा भी शुरू की गई थी TaskScheduler- यह थ्रेड्स के शीर्ष पर एक प्रकार का अमूर्त है जो बताता है कि कार्य कहां चलाना है। इस मामले में, डिजाइन स्तर पर रद्दीकरण समर्थन डिजाइन किया गया था। .NET में लाइब्रेरी के लगभग सभी ऑपरेशंस को पास CancellationTokenकिया जा सकता है। यह सभी भाषाओं के लिए काम नहीं करता है: उदाहरण के लिए, कोटलिन में आप कार्य को पूर्ववत कर सकते हैं, लेकिन .NET में आप नहीं कर सकते। समाधान उन लोगों के बीच जिम्मेदारी का विभाजन हो सकता है जो कार्य को रद्द करते हैं, और स्वयं कार्य। जब आप एक कार्य प्राप्त करते हैं, तो आप इसे स्पष्ट रूप से अन्यथा रद्द नहीं कर सकते - आपको इसे पारित करना होगा CancellationToken

एक विशेष ऑब्जेक्ट TaskCompletionSoureआपको पुराने एपीआई को आसानी से अनुकूलित करने की अनुमति देता है जो इवेंट-आधारित अतुल्यकालिक पैटर्न या अतुल्यकालिक प्रोग्रामिंग मॉडल से जुड़े हैंएक दस्तावेज़ है जिसे आपको कार्यों में प्रोग्राम करने पर अवश्य पढ़ना चाहिए। यह तस्स के बारे में सभी समझौतों का वर्णन करता है। उदाहरण के लिए, किसी भी विधि, कार्य को लौटाने, उसे चालू स्थिति में वापस करना चाहिए, जिसका अर्थ है कि यह नहीं हो सकता है Created, जबकि इस तरह के सभी कार्यों को समाप्त होना चाहिए Async

निरंतरता का मेल


Task ourMethod() {
  return Task.RunSynchronously(() =>{
    ... //synchronous code
  })
  .ContinueWith(_ =>{
    Foo(); //continuation 1
  })
  .ContinueWith(_ =>{
    Bar(); //continuation 2
  })
  .ContinueWith(_ =>{
    Baz(); //continuation 3
  })
}

संयोजन के लिए, कॉलबैक नरक को ध्यान में रखते हुए , यह कम से कम परिवर्तनों के साथ कोड को दोहराने की उपस्थिति के बावजूद अधिक रैखिक रूप में प्रकट हो सकता है। ऐसा लगता है कि कोड इस तरह से सुधार कर रहा है, लेकिन यहां भी नुकसान हैं।

कार्य प्रारंभ करें और जारी रखें


Task.Factory.StartNew(Action, 
  TaskCreationOptions, 
  TaskScheduler, 
  CancellationToken
)
Task.ContinueWith(Action<Task>, 
  TaskContinuationOptions, 
  TaskScheduler, 
  CancellationToken
)

मानक कार्य लॉन्च के दौरान हमें तीन मापदंडों की ओर मुड़ते हैं: पहला कार्य शुरू करने के लिए विकल्प हैं, दूसरा वह है जिस schedulerपर कार्य शुरू किया गया है, और तीसरा - CancellationToken



TaskScheduler बताता है कि कार्य कहाँ शुरू होता है और एक ऐसी वस्तु है जिसे आप स्वतंत्र रूप से ओवरराइड कर सकते हैं। उदाहरण के लिए, आप एक विधि को ओवरराइड कर सकते हैं Queue। यदि आप करते TaskSchedulerहैं thread pool, तो विधि Queueएक थ्रेड लेता है thread poolऔर आपके कार्य को वहां भेजता है।

यदि आप schedulerमुख्य धागे को लेते हैं, तो यह सब कुछ एक कतार में रखता है, और कार्यों को मुख्य धागे पर क्रमिक रूप से निष्पादित किया जाता है। हालाँकि, समस्या यह है कि .NET में आप बिना पास किए कार्य निष्पादित कर सकते हैं TaskScheduler। सवाल उठता है: तो फिर .NET कैसे गणना करता है कि इसे किस कार्य के लिए पारित किया गया था? जब कार्य StartNewअंदर से शुरू होता हैAction, ThreadStaticहम उसे दिया है कि Currentएक में प्रदर्शित किया TaskScheduler

यह डिज़ाइन निहित संदर्भ के कारण विवादास्पद लगता है। ऐसे मामले थे जब TaskSchedulerइसमें अतुल्यकालिक कोड निहित था जो कहीं TaskScheduler.Currentऔर गहराई से विरासत में मिला और एक अन्य अनुसूचक के साथ ओवरलैप किया गया, जिसके कारण गतिरोध पैदा हुआ। इस मामले में, आप विकल्प का उपयोग कर सकते हैं TaskCreationOption.HideScheduler। यह एक खतरे की घंटी है जो कहती है कि हमारे पास कुछ विकल्प हैं जो ThreadStaticसेटिंग को ओवरराइड करता है।

निरंतरता के साथ सब कुछ समान है। सवाल उठता है: यह TaskSchedulerनिरंतरताओं के लिए कहां से आता है ? सबसे पहले, यह उस पद्धति में लिया जाता है जिसमें आपने शुरू किया था Continuation। इसे TaskSchedulerथ्रेडस्टैटिक से भी लिया गया है। यह महत्वपूर्ण है कि async / प्रतीक्षा के लिए, निरंतरता बहुत अलग तरीके से काम करती है।



हम मापदंडों की ओर मुड़ते हैं TaskCreationOptionsऔर TaskContinuationOptions। उनकी मुख्य समस्या यह है कि उनमें से बहुत सारे हैं। इनमें से कुछ पैरामीटर एक-दूसरे को रद्द करते हैं, कुछ परस्पर अनन्य हैं। इन सभी मापदंडों का उपयोग सभी संभव संयोजनों में किया जा सकता है, इसलिए यह सब कुछ ध्यान में रखना मुश्किल है जो लालसा के साथ हो सकता है। इन विकल्पों में से कुछ पूरी तरह से समझ से बाहर काम करते हैं।



उदाहरण के लिए, पैरामीटर ExecuteSynchronouslyऔर RunContinuationsAsynchronouslyदो संभावित अनुप्रयोग विकल्पों का प्रतिनिधित्व करते हैं, लेकिन क्या निरंतरता को सिंक्रोनाइज़ किया जाएगा या अतुल्यकालिक रूप से कई चीजों पर निर्भर करता है जिनके बारे में आपको पता नहीं होगा।



एक और उदाहरण: हमने कार्य शुरू किया, निरंतरता का शुभारंभ किया और साथ ही साथ दो मापदंड दिएTaskContinuations.ExecuteSynchronously, जिसके बाद उन्होंने एसिंक्रोनस रूप से निरंतरता शुरू की। क्या इसे उसी स्टैक में निष्पादित किया जाएगा जहां पिछला कार्य समाप्त होता है, या इसे स्थानांतरित किया जाएगा thread pool? इस मामले में, एक तीसरा विकल्प होगा: यह निर्भर करता है।



TaskCompletionSource


विचार करें TaskCompletionSource। जब आप कार्य बनाते हैं, तो आप इसके परिणाम SetResultको पिछले अतुल्यकालिक पैटर्न को कार्य की दुनिया में बदलने के लिए निर्धारित करते हैं। आप TaskCompletionSourceअनुरोध कर सकते हैं tcs.Task, और finishजब आप कॉल करेंगे तो यह कार्य एक स्थिति में चला जाएगा tcs.SetResult। हालांकि, यदि आप इसे थ्रेड पूल पर चलाते हैं, तो आपको गतिरोध मिलेगा । सवाल यह है कि अगर हमने सिंक्रोनाइज़ होने पर भी कुछ नहीं लिखा तो क्यों?



हम TaskCompletionSourceएक नया कार्य शुरू करते हैं, और हमारे पास एक दूसरा सूत्र है जो इस कार्य में कुछ शुरू करता है। यह सौ मिलीसेकंड के लिए उम्मीद से अधिक हो जाता है और गिर जाता है। फिर हमारा मुख्य धागा - हरा - प्रतीक्षा करने के लिए जाता है और यह है। वह स्टैक जारी करता है, स्टैक लटका रहता है, एक निरंतरता में कहा जाता हैtask.Waitजब tcsउजागर हो।

नीले धागे में हम tcsऔर फिर सबसे दिलचस्प हो जाते हैं। .NET के आंतरिक विचारों के आधार पर, उनका TaskCompletionSourceमानना ​​है कि इस की निरंतरता को tcsसिंक्रोनाइज़ किया जा सकता है, यानी सीधे एक ही स्टैक पर, फिर task.Waitउसी स्टैक पर इसे सिंक्रोनाइज़ किया जाता है। यह बहुत अजीब है, इस तथ्य के बावजूद कि हमने कहीं भी नहीं लिखा है ExecuteSynchronouslyयह संभवत: सिंक्रोनस और एसिंक्रोनस कोड को मिलाने में समस्या है।



इसके साथ एक और समस्या यह TaskCompletionSourceहै कि जब हम लॉक केSetResult तहत कॉल करते हैं , तो आप मनमाने कोड को कॉल नहीं कर सकते हैं, क्योंकि लॉक के तहत आप केवल कुछ छोटी सी गतिविधि कर सकते हैं। कुछ एक्शन-एस के नीचे चलाएंयह असंभव है कि वे कहाँ से आए हैं। इस समस्या को हल कैसे करें?

var  tcs  =  new   TaskCompletionSource<int>(
       TaskContinuationsOptions.RunContinuationsAsynchronously  
) ;
lock(mylock)
{  
    tcs.SetResult(O); 
});

यह TaskCompletionSourceकेवल पुस्तकालयों में टास्क कोड के अनुकूलन के लिए उपयोग करने के लायक है। लगभग सब कुछ प्रतीक्षित के माध्यम से हल किया जा सकता है। इस मामले में, पैरामीटर को "टास्क कॉमप्लेक्शनस्रोस.ऑनकांउटसैशनएस्ट्रिंक्रियसली" के रूप में निर्धारित करने की हमेशा दृढ़ता से सिफारिश की जाती है आपको लगभग हमेशा एक निरंतरता को चलाने की आवश्यकता होती है। इस मामले में, आपके tcs.SetResultपास कुछ है जिसके तहत कुछ भी लॉन्च नहीं किया जाएगा।



निरंतरता को सिंक्रोनाइज़ क्यों किया जाना चाहिए? क्योंकि यह RunContinuationsAsynchronouslyनिम्नलिखित को संदर्भित करता है ContinueWith, और हमारा नहीं। उसे हमारे से संबंधित करने के लिए, आपको निम्नलिखित लिखने की आवश्यकता है:



यह उदाहरण दिखाता है कि पैरामीटर कैसे सहज नहीं हैं, वे एक-दूसरे के साथ कैसे अंतरंग हैं, कैसे वे संज्ञानात्मक जटिलता का परिचय देते हैं - यह लिखना इतना मुश्किल है।

माता-पिता-बच्चे पदानुक्रम


Task.Factory.StartNew(() => 
{
  //... some parent activity

   Task.Factory.StartNew(() => {
      //... some child activity
   })

})
.ContinueWith(...) // don’t wait for child

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



आप लिखेंगे TaskCreationOptions.AttachedToParentतो ContinueWithइंतजार रहेगा। आप अपने उत्पादों में इस संपत्ति का उपयोग कर सकते हैं। मुझे लगता है कि हर कोई एक उदाहरण के साथ आ सकता है जिसमें कार्यों का एक पदानुक्रम है, कार्य के साथ सबटस्क के लिए इंतजार कर रहा है, और इसके उपशीर्षक के लिए सबस्कॉस्क। कहीं भी लिखने की आवश्यकता नहीं है WaitForChildren, यह प्रतीक्षा असिंक्रोनस रूप से होती है। यही है, माता-पिता के कार्य का शरीर समाप्त हो जाता है, और उसके बाद माता-पिता के कार्य को पूर्ण नहीं माना जाता है, तब तक अपनी निरंतरता शुरू नहीं करता है जब तक कि बच्चे काम नहीं करते।

Task.Factory.StartNew(() => 
{
  //... some parent activity
  Foo(); 

})
.ContinueWith(...) // still wait for child

void Foo() { 
   Task.Factory.StartNew(() => {
      //... parent task to attach is in ThreadStatic
   }, TaskCreationOptions.AttachedToParent); 
}

एक ऐसी समस्या हो सकती है जिसमें कार्य को कहीं स्थानांतरित ThreadStaticकर दिया जाता है, फिर आपके द्वारा शुरू की गई हर चीज AttachedToParentको इस मूल कार्य में जोड़ दिया जाएगा, जो कि एक खतरे की घंटी है।

Task.Factory.StartNew(() => 
{
  //... some parent activity

  Foo();
}, TaskCreationOptions.DenyChildAttach)
.ContinueWith(...) // don’t wait for child

void Foo() { 
   Task.Factory.StartNew(() => {
      //... some child activity
   }, TaskCreationOptions.AttachedToParent); 
}

दूसरी ओर, एक विकल्प है जो पिछले विकल्प को रद्द करता है DenyChildAttachऐसा अनुप्रयोग अक्सर होता है।

Task.Run(() => 
{
  //... some parent activity

  Foo(); 

})
.ContinueWith(...) //don’t wait for child

void Foo() { 
   Task.Factory.StartNew(() => {
      //... some child activity
    }, TaskCreationOptions.AttachedToParent); 
}

यह याद रखने योग्य है कि Task.Runयह शुरू करने का मानक तरीका है, जो डिफ़ॉल्ट रूप से होता है DenyChildAttach

अंतर्निहित संदर्भ जो आप में ThreadStaticजटिलता जोड़ता है। आप यह नहीं समझते कि कार्य कैसे कार्य करता है, क्योंकि आपको संदर्भ जानने की आवश्यकता है। एक और समस्या जो उत्पन्न हो सकती है, वह async / प्रतीक्षा की निष्क्रिय स्थिति से संबंधित है। ऐसा इसलिए है क्योंकि async / प्रतीक्षा में आपके पास कार्य नहीं हैं, लेकिन क्रियाएँ हैं। निरंतरता ईमानदार कार्य नहीं है, बल्कि कार्रवाई है। जब आप async / प्रतीक्षा कोड लिखते हैं, तो आपको इसका उपयोग करने की आवश्यकता नहीं है AttachedToParent, क्योंकि आप प्रतीक्षा के माध्यम से प्रतीक्षा करने के लिए कार्यों को स्पष्ट रूप से टाई करते हैं, और यह सही दृष्टिकोण है।



आपके पास निरंतरता शुरू करने के छह विकल्प हैं। आपने टास्क लॉन्च किया, लॉन्च कियाContinueWithप्रश्न: इस निरंतरता की क्या स्थिति होगी? पाँच संभावित उत्तर हैं:

  • सामान्य निरंतरता को सफलतापूर्वक पूरा किया जाएगा;
  • कार्य त्रुटि में होगा;
  • निरस्तीकरण होगा;
  • कार्य पूर्णता तक नहीं पहुंचेगा, यह किसी प्रकार के अंग में होगा;
  • विकल्प - "निर्भर करता है"।



इस मामले में, कार्य "रद्द" स्थिति में होगा, हालांकि कहीं भी "रद्द" शब्द कहीं नहीं है। यहां हम रिसेप्शन फेंकते हैं और कुछ भी नहीं करते हैं। समस्या यह है कि जब आप किसी अन्य कोड को बहुत अधिक विकल्पों के साथ पढ़ते हैं - भले ही आप इन विकल्पों के बारे में 10 मिनट पहले जानते हों - आप अभी भी भूल जाते हैं कि यहां क्या होता है। इसलिए लिखो मत।

रद्द करना



Task.Factory.StartNew(() => 
{
    throw new OperationCanceledException(); 
});

                                                      Failed

कार्य की शुरुआत में तीसरा पैरामीटर kancellation है। आप लिखते हैं OperationCanceledException, अर्थात् , एक विशेष क्रिया जो "रद्द" स्थिति में कार्य करती है। इस स्थिति में, कार्य "विफल" स्थिति में होगा, क्योंकि सभी OperationCanceledExceptionसमान नहीं हैं।

Task.Factory.StartNew(() => 
{
    throw new OperationCanceledException(cancellationToken); 
}, cancellationToken);

                                                      Canceled

कार्य करने में सक्षम होने के लिए Canceled, आपको इसे OperationCanceledExceptionअपने रद्द करने के साथ फेंकने की आवश्यकता है वास्तविकता में, आप कभी भी स्पष्ट रूप से ऐसा नहीं करते हैं, लेकिन इसे इस तरह से करें:

Task.Factory.StartNew(() => 
{
    cancellationToken.ThrowIfCancellationRequested(); 
}, cancellationToken);
                                                       Canceled

क्या रद्दीकरण को रोकना आवश्यक है? कहीं कार्य के अंदर, आप जांचते हैं कि किसी ने आपको हटा दिया है: फेंक फेंक रद्द करें, फिर कार्य राज्य में चला जाता है Canceledया किसी ने रन समय में "रद्द करें" पर क्लिक किया और कार्य को रद्द कर दिया। JetBrains में हमारा अभ्यास बताता है कि आपको इन टोकन के बीच अंतर करने की आवश्यकता नहीं है। यदि आपको एक OperationCanceledException मिलती है - एक विशेष प्रकार जो तब होता है जब कुछ रद्दीकरण हुआ हो, तो आप इसे अलग कर सकते हैं। इस मामले में, आपको बस कार्य को सामान्य रूप से पूरा करने की आवश्यकता है, लॉग इन न करें और जब आप निष्पादन प्राप्त करें - लॉग इन करें।

गहरा ढेर


Task.Factory.StartNew(() => 
{
    Foo();
}, cancellationToken);

  void Foo() { 
     Bar() {
       ...
          Baz() {
             //how to get cancellation token?
          } 
    }
}

मान लीजिए कि आपके पास एक गहरा ढेर है। यह CancellationTokenएकमात्र स्पष्ट पैरामीटर है जिस पर हमने चर्चा की। यह बिल्कुल सभी पदानुक्रमों के माध्यम से हर जगह प्रसारित किया जाना चाहिए। मुझे क्या करना चाहिए, अगर एक गहरी पदानुक्रम की उपस्थिति में, आपको रिसेप्शन को फेंकने के लिए, अपने काम को कहीं सबसे कम स्तर पर रद्द करने की आवश्यकता है? ऐसी ही एक खास ट्रिक है जिसका हम इस्तेमाल करते हैं। उसे कहा जाता है AsyncLocal

static AsyncLocal<Cancelation> asyncLocalCancellation;

Task.Factory.StartNew(() => 
{
     asyncLocalCancellation.Set(cancellationToken) 
    Foo();
}, cancellationToken); // use AsyncLocal to put cancellation int

  void Foo() { 
     async Bar() {
      ...
         Baz() {
             asyncLocalCancellation.Value.CheckForInterrupt(); 
         }
   } 
}

यह ThreadStaticकेवल उसी विशेष के समान है ThreadLocalजो async / वेट कोड ट्रिप से बचता है। चूँकि आपका कोड अतुल्यकालिक है, और आपके पास यह kancellation है, तो आप इसे डालते हैं AsyncLocal, और कहीं गहरे स्तर पर आप " CheckForInterrupt Throw If Cancellation Requested" कह सकते हैं फिर, यह एकमात्र पैरामीटर CancellationTokenहै जिसे संपूर्ण कोड को पूरी तरह से धब्बा करने की आवश्यकता है, लेकिन, मेरी राय में, अधिकांश कार्यों के लिए आपको बस यह जानना होगा कि क्या हुआ OperationCanceledException, और इससे एक निष्कर्ष निकलता है कि कौन सा राज्य: रद्द या विफल।

संज्ञानात्मक जटिलता


Task.Factory.StartNew(Action, 
    TaskCreationOptions, 
    TaskScheduler, 
    CancellationToken
)
                                                   JetBrains.Lifetimes

lifetime.Start(TaskScheduler, Action) //puts lifetime in AsyncLocal

lifetime.StartMainRead(Action) 
lifetime.StartMainWrite(TaskScheduler, Action) 
lifetime.StartBackgroundRead(TaskScheduler, Action)

कार्य शुरू करते समय कोड को पढ़ना जितना कठिन होता है, त्रुटि का खतरा उतना ही अधिक होता है। एक वर्ष के बाद कोड को देखते हुए, आप यह भूल जाएंगे कि यह क्या करता है, क्योंकि बड़ी संख्या में पैरामीटर हैं। लेकिन हमारे पास JetBrains.Lifetimes पुस्तकालय है , जो आधुनिक जीवनकाल, अच्छी तरह से अनुकूलित कैंसेलेशनटोकन प्रदान करता है, जिसके साथ स्टार्ट विधि को फिर से लिखा गया था और कोड के टुकड़े को दोहराने के साथ समस्या को हल किया गया था, जैसा कि Task.Factory.StartNewऔर TaskCreationOptions

शेड्यूलर्स की एक छोटी संख्या है जो आपको रीड लॉक के साथ मुख्य थ्रेड पर किसी कार्य को शेड्यूल करने की अनुमति देती है। यही है, रीड लॉक कुछ ऐसा नहीं है जिसे आप स्पष्ट रूप से चुनते हैं, यह एक विशेष अनुसूचक है जो आपके कोड को मुख्य लॉक पर रीड लॉक के साथ शेड्यूल करता है, साथ ही राइट लॉक, बैकग्राउंड थ्रेड के साथ मुख्य धागा - और अब फेरबदल शुरू करने के लिए तरीके बहुत सरल हो गए हैं। एक ही समय में, जीवनकाल स्वचालित रूप से रद्द कर देता है AsyncLocal, कोड को काफी सरल करता है।



आइए देखें कि इन समस्याओं को हल करने के लिए कैसे async / प्रतीक्षा करें , और वे किन समस्याओं का परिचय देते हैं।

इस उदाहरण में, कोड का हिस्सा तुल्यकालिक रूप से निष्पादित किया जाता है, फिर प्रतीक्षा और अतुल्यकालिक कोड। सबसे पहले, यह अच्छा है कि कोड ( बॉयलर-प्लेट ) के बहुत कम दोहराए जाने वाले टुकड़े हैं । दूसरे, यह अच्छा है कि एसिंक्रोनस कोड सिंक्रोनस कोड के समान है, यह ठीक वैसा ही है जैसे एसिंक्स / वेट के लिए है । आप उसी तरह से एसिंक्रोनस रूप से लिख सकते हैं जैसे आपने थ्रेड्स को उठाए बिना, सिंक्रोनस को लिखा था।

इस मामले में संकलक क्या तैनात करेगा? सिंक्रोनस कोड तुल्यकालिक रूप से निष्पादित करेगा, जिसके बाद कार्य सिंक्रोनस निष्पादित InnerAsyncकरेगा, जहां से विशेष गेटवाटर ऑब्जेक्ट आएगा। इस मामले में, हम रुचि रखते हैं TaskAwaiter। आप किसी भी वस्तु के लिए अपना वेटर लिख सकते हैं। नतीजतन, हम कार्य को पूरा करने के लिए प्रतीक्षा करते हैं InnerAsyncऔर सिंक्रोनाइज़ करते हैं continuationCode। यदि कार्य पूरा नहीं हुआ, तो निरंतरता को प्रसंग शेड्यूलर पर शेड्यूल किया गया है । यह हो सकता है कि, भले ही आपने प्रतीक्षा के बारे में लिखा हो , बिलकुल सब कुछ सिंक्रोनस कहलाएगा।

async Task MyFuncAsync() { 
  synchronousCode();
   await InnerAsync();
   await Task.Yield(); //guaranteed !IsCompleted 
   continuationCode();
}

एक चाल है Task.Yield- यह एक विशेष कार्य है जो यह सुनिश्चित करता है कि इसका वेटर हमेशा आपके पास वापस नहीं आएगा IsCompleted। तदनुसार, continuationइसे इस स्थान पर सिंक्रोनस नहीं कहा जाएगा। UI थ्रेड के लिए, यह महत्वपूर्ण हो सकता है क्योंकि आप इस थ्रेड को बड़ी मात्रा में नहीं लेते हैं।



निरंतरता के लिए एक धागा कैसे चुनें? Async / प्रतीक्षित दर्शन यह है: आप एसिंक्रोनस कोड को सिंक्रोनस के समान लिखते हैं। यदि आपके पास एक थ्रेड पूल है , तो इससे आपको कोई फर्क नहीं पड़ता है - निरंतरता कोड को दूसरे थ्रेड पर निष्पादित किया जाएगा। इस बात की परवाह किए बिना कि InnerAsyncजब आप इंतजार कर रहे थे या नहीं तो यह पूरा हो गया है , आपको यूआई थ्रेड पर निष्पादित करने के लिए सब कुछ चाहिए।

कार्य प्रतीक्षा के लिए तंत्र निम्नानुसार है: इसे लिया जाता है static, इसे कहा जाता हैSynchronizationContextऔर इससे निर्माण होता है TaskSchedulerSynchronizationContext पोस्ट विधि के साथ एक चीज है, जो विधि के समान है Queueवास्तव में TaskScheduler, जो पहले था, वह बस लेता है SynchronizationContextऔर पोस्ट के माध्यम से उस पर अपना कार्य करता है।

async Task MyFuncAsync() { 
  synchronousCode();

    await InnerAsync().ConfigureAwait(false);
    continuationCode(); 
}

एक पैरामीटर का उपयोग करके इस व्यवहार को बदलने का एक तरीका है ContinueOnCapturedContext.NET में सबसे घृणित API कहा जाता है ConfigureAwaitइस मामले में, एपीआई एक विशेष वेटर बनाता है, उससे अलग TaskAwaiterजो निरंतरता को बदलता है, यह उसी धागे पर चलता है, उसी संदर्भ में जिसमें विधि समाप्त हो गई InnerAsync और जहां कार्य समाप्त हो गया।

async Task MyFuncAsync() { 
  synchronousCode();

    await InnerAsync().ConfigureAwait(continueOnCapturedContext: false); 
    continuationCode(); //code must be absolutely context-agnostic
}

इंटरनेट पर सलाह की एक पागल राशि है: यदि आपके पास गतिरोध है , तो कृपया अपने सभी कन्फिगरएविट कोड को स्मियर करें और सबकुछ ठीक हो जाएगा। यह गलत तरीका है। ConfigureAwaitउन मामलों में उपयोग किया जा सकता है जहां आप प्रदर्शन में सुधार करना चाहते हैं, या विधि के अंत में, कुछ पुस्तकालय विधियों में।

गतिरोध


async Task MyFuncAsync() { //UI thread 
  synchronousCode();

    await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); 
    continuationCode();
}
myFuncAsync().Wait() //on UI thread

यह एक क्लासिक गतिरोध हैUI थ्रेड पर, उन्होंने दस सेकंड इंतजार किया और किया Waitआपने जो किया है Wait, उसके कारण इसे continuationCodeकभी लॉन्च नहीं किया जाएगा, Waitइसलिए यह कभी वापस नहीं आएगा। यह सब बहुत शुरुआत में होता है।

async Task OnBluttionClick() { //UI thread 
  int v = Button.Text.ParseInt();

    await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); 
  Button.Text.Set((v+1).ToString());
}
myFuncAsync().Wait() //on UI thread

कल्पना कीजिए कि यह कुछ वास्तविक गतिविधि है। हमने बटन पर क्लिक किया, इसे लिया Button.ParseInt, इंतजार किया , लिखा ConfigureAwaitहमने कहा: "कृपया हमारे UI स्ट्रीम को बंद न करें, जारी रखें।" समस्या यह है कि हम ConfigureAwaitUI थ्रेड पर निष्पादित होने के बाद दूसरा भाग भी चाहते हैं, क्योंकि यह प्रतीक्षा का दर्शन है । यही है, आपका एसिंक्रोनस कोड सिंक्रोनस कोड के समान दिखता है, और उसी संदर्भ में चलता है। इस मामले में, निश्चित रूप से, एक त्रुटि होगी। और इसके अलावा Button.Text.Setकिसी भी विधि कॉल की संख्या हो सकती है जो उनके संदर्भ को भी मानती है। इस स्थिति में क्या करना है? तुम यह केर सकते हो:

async Task MyFuncAsync() { //UI thread 
  synchronousCode();

    await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); 
    continuationCode(); //The same UI context
}
PumpUntil(() => task.IsCompleted);
//VS synchronization contexts always pump on any Wait

UI थ्रेड के साथ, आपको उन Waitथ्रेड्स पर ऐसा करने पर प्रतिबंध लगाना होगा, जिनमें एक सामान्य संदेश कतार हो। करने Waitया लिखने के बजाय ConfigureAwait, आप संदेशों की इस कतार को पंप कर सकते हैं, और साथ ही, निरंतरता को भी पंप किया जाएगा। यदि आप सिंक्रोनस और एसिंक्रोनस कोड को नहीं मिला सकते हैं, तो आपको उन्हें मिश्रण नहीं करना चाहिए। लेकिन कभी-कभी इससे बचा नहीं जा सकता है।

उदाहरण के लिए, आपके पास पुराना कोड है, और आपको उन्हें मिलाना होगा, फिर आप UI स्ट्रीम को पंप करेंगे। विजुअल स्टूडियो ने उम्मीदों पर यूआई थ्रेड को पंप किया, यह SynchronizationContextथोड़ा बदल गया। यदि आप किसी पर WaitHandle में जाते हैं Wait, तो जब आप लटकाते हैं, तो आपका UI स्ट्रीम पंप होता है। इस प्रकार, वे दौड़ के पक्ष में गतिरोध और दौड़ के बीच चयन करते हैं

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

संदर्भ बदलें


async Task MyFuncAsync() { 
  synchronousCode(); // on initial context

    await myTaskScheduler;
    continuationCode(); //on scheduler context 
}

Async / wait का उपयोग करने का एक और दिलचस्प तरीका है आप लिख सकते हैं Awaiterपर schedulerऔर धागे पर कूद। मैंने विजुअल स्टूडियो में पोस्ट पढ़ी, उन्होंने बहुत लंबे समय तक लिखा कि विधि के बीच में आगे और पीछे कूदना अच्छा नहीं है, लेकिन अब वे इसे स्वयं करते हैं। विजुअल स्टूडियो में एक एपीआई है जो शेड्यूलर्स के माध्यम से थ्रेड पर कूदता है। सामान्य उपयोग के लिए, यह करना अच्छा नहीं है।

संरचित संगति


async Task MyFuncAsync() { 
  synchronousCode(); // on initial context

    await Task.Factory.StartNew(() => {...}, myTaskScheduler);
    continuationCode(); //on initial context 
}

नए संदर्भ में सुविधाजनक विसर्जन के लिए और पुराने पर लौटने के लिए, कुछ संरचनात्मक प्रतियोगिता या संरचनात्मक समानता का निर्माण किया जाना चाहिए। उदाहरण के लिए, साठ के दशक में, GoTo ऑपरेटर को हानिकारक माना जाता था क्योंकि यह संरचनात्मकता का उल्लंघन करता था। तो यह यहाँ है। धागे पर कूदना संरचनात्मक का उल्लंघन करता है। हैरानी की बात है, एक async राज्य मशीन का उपयोग करना एक अच्छा तरीका है। यही है, जहां आपकी सामान्य संरचना का उल्लंघन किया जाता है, आप GoTo पर कूदते हैं, आप थ्रेड संरचना का उल्लंघन कर सकते हैं: प्रतीक्षा करें , इसे टैग के साथ मिलाएं यह एक बहुत ही अजीब और दुर्लभ स्थिति है जब आपको ऐसा करने की आवश्यकता होती है। फिर भी, उसी संदर्भ में रिटर्न का इंतजार करना बेहतर है। इस प्रकार, थ्रेड पूल में एक ही थ्रेड नहीं होगा, लेकिन यह मूल रूप से एक ही संदर्भ है।

अनुक्रमिक व्यवहार


समानांतर निष्पादन के समान प्रतीक्षा क्यों नहीं की जाती है? प्रतीक्षा निष्पादन अनुक्रमिक निष्पादन है। इस मामले में, हम पहला कार्य शुरू करते हैं, इसके लिए प्रतीक्षा करते हैं, दूसरा कार्य शुरू करते हैं - हम प्रतीक्षा करते हैं। हमारी कोई समानता नहीं है। अधिकांश उपयोगों के लिए, समानता की आवश्यकता नहीं है। समानतावाद स्वयं अनुक्रम से अधिक जटिल है। सीरियल कोड समानांतर की तुलना में सरल है, यह एक स्वयंसिद्ध है। लेकिन कभी-कभी आपको समानांतर कोड में कुछ चलाने की आवश्यकता होती है, और आप इसे इस तरह से करते हैं:

async Task MyAsync() {

  var task1 = StartTask1Async();
  await task1;

  var task2 = StartTask2Async();
  await task2; 
}

समवर्ती व्यवहार


async Task MyAsync() {
  var task1 = StartTask1Async();
  var task2 = StartTask2Async();

  await task1;
  await task2; 
}

यहां कार्य समानांतर में शुरू होते हैं। यह स्पष्ट है कि विधियाँ कार्य चलाने की स्थिति में तुरंत लौट सकती हैं, फिर कोई समानता नहीं होगी। बता दें कि दोनों टास्क को अंजाम देते हैं। और आप पहले कार्य के लिए इंतजार कर रहे थे, फिर पहले इंतजार पर उड़ान भरी। यानी जैसे ही आपने लिखा await task1, आपने उतार दिया और प्रक्रिया नहीं की exception task2दिलचस्प है, यह बिल्कुल वैध कोड है। और यह कोड है जिसने .NET को इस तथ्य के लिए प्रेरित किया कि संस्करण 4.5 में निष्पादन के साथ काम करने का व्यवहार बदल गया है।

उपवाद सम्भालना


async Task MyAsync() {
  var task1 = StartTask1Async();
  var task2 = StartTask2Async(); 

  await task1;
  await task2;

  // if task1 throws exception and task2 throws exception we only throw and
  // handle task1’s exception

  //4.0 -> 4.5 framework: unhandled exceptions now don’t crush process
  //still visible in UnobservedExceptionHandler
}

पहले, अनहैन्ड एक्जीक्यूशन ने बस प्रक्रिया को फेंक दिया, और अगर आपने कुछ निष्पादन को नहीं पकड़ा UnobservedExceptionHandler(यह कुछ ऐसा भी है staticजिसे आप अनुसूचियों से जोड़ सकते हैं), तो यह प्रक्रिया निष्पादित नहीं हुई। अब यह बिल्कुल वैध कोड है। हालाँकि .NET ने अपने व्यवहार को बदल दिया, लेकिन उसने विपरीत दिशा में व्यवहार को वापस करने के लिए सेटिंग को बनाए रखा।

async  Task  MyAsync(CancellationToken cancellationToken)  {  

  await  SomeTask1  Async(cancellationToken); 
 
  await  Some Task2Async( cancellation  Token); 
  //you should always pass use async API with cancelationToken  if possible 
} 
  
try { 
    await  MyAsync( cancellation  Token); 
} catch (OperationException e) { // do nothing: OCE happened
} catch (Exception e) { 
    log.Error(e);
}

देखें कि निष्पादन की प्रक्रिया कैसे चलती है। रद्दीकरण टोकेन-एस को प्रेषित किया जाना चाहिए, सभी कोड को रद्द करना "रद्द करना" आवश्यक है। Async का सामान्य व्यवहार यह है कि आप कहीं भी जाँच नहीं करते हैं Task.Status ancellationToken, आप एसिंक्रोनस कोड के साथ उसी तरह काम करते हैं जैसे कि सिंक्रोनस के साथ। यही है, रद्दीकरण के मामले में, आपको एक निष्पादन मिलता है, और इस मामले में, जब आप इसे प्राप्त करते हैं तो आप कुछ भी नहीं करते हैं OperationCanceledException

रद्द और दोषपूर्ण की स्थिति के बीच अंतर यह है कि आपको प्राप्त नहीं हुआ OperationCanceledException, लेकिन सामान्य निष्पादन। और इस मामले में, हम इसे प्रतिज्ञा कर सकते हैं, आपको बस एक निष्पादन प्राप्त करने और इसके आधार पर निष्कर्ष निकालने की आवश्यकता है। यदि आप कार्य को स्पष्ट रूप से शुरू करते हैं, तो टास्क के माध्यम से, आप उड़ गए होंगे AggregateExceptionऔर async में, मामले में वे AggregateExceptionहमेशा बहुत पहले निष्पादन को फेंक देते हैं जो इसमें था (इस मामले में - OperationCanceled)।

प्रयोग में


सिंक्रोनस विधि


DataTable<File, ProcessedFile> sharedMemory;

// in any thread
void SynchronousWorker(...) {
  File f = blockingQueue.Dequeue(); 
  ProcessedFile p = ProcessInParallel(f);

  lock (_lock) { 
    sharedMemory.add(f, p);
  } 
}

उदाहरण के लिए, एक दानव ReSharper में काम करता है - एक संपादक जो आपके लिए फ़ाइल को चिह्नित करता है। यदि फ़ाइल को संपादक में खोला जाता है, तो कुछ गतिविधि है जो इसे एक अवरुद्ध कतार में रखती है। हमारी प्रक्रिया workerवहां से पढ़ती है, जिसके बाद यह इस फ़ाइल के साथ विभिन्न कार्यों का एक गुच्छा करता है, इसे चिह्नित करता है, पार्स करता है, बनाता है, जिसके बाद इन फ़ाइलों को जोड़ा जाता है sharedMemoryएक sharedMemoryलॉक के साथ , अन्य तंत्र पहले से ही इसके साथ काम कर रहे हैं।

अतुल्यकालिक विधि


जब अतुल्यकालिक करने के लिए कोड को फिर से लिखने, हम सब से पहले यह स्थान ले लेगा voidसाथ async Taskअंत में "Async" शब्द लिखना न भूलें। सभी एसिंक्रोनस विधियों को एसिंक्स में समाप्त होना चाहिए - यह एक सम्मेलन है।

DataTable<File, ProcessedFile> sharedMemory;
// in any thread
async Task WorkerAsync(...) {

  File f = blockingQueue.Dequeue(); 

  ProcessedFile p = ProcessInParallel(f);

  lock (_lock) { 
    sharedMemory.add(f, p);
  } 
}

उसके बाद, आपको हमारे साथ कुछ करने की ज़रूरत है blockingQueue। जाहिर है, अगर कुछ तुल्यकालिक आदिम है, तो कुछ अतुल्यकालिक आदिम होना चाहिए।



इस आदिम को चैनल कहा जाता है: वे चैनल जो पैकेज में रहते हैं System.Threading.Channels। आप सीमित और असीमित चैनल बना सकते हैं, जिसे आप अतुल्यकालिक रूप से प्रतीक्षा कर सकते हैं। इसके अलावा, आप "शून्य" के मान के साथ एक चैनल बना सकते हैं, अर्थात इसमें बफर बिल्कुल नहीं होगा। इस तरह के चैनलों को रेज़िक्वेंट चैनल कहा जाता है और गो और कोटलिन में सक्रिय रूप से प्रचारित किया जाता है। और सिद्धांत रूप में, अगर अतुल्यकालिक कोड में चैनलों का उपयोग करना संभव है, तो यह एक बहुत अच्छा पैटर्न है। यही है, हम कतार को चैनल में बदलते हैं जहां तरीके हैं ReadAsyncऔर WriteAsync

ProcessInParallel समानांतर कोड का एक गुच्छा है जो किसी फ़ाइल की प्रोसेसिंग करता है और इसे चालू करता हैProcessedFileक्या एसिंक्स हमें एसिंक्रोनस नहीं लिखने में मदद कर सकता है, लेकिन समानांतर कोड अधिक कॉम्पैक्ट रूप से?

समानांतर कोड को सरल बनाएं


इस तरह से कोड को फिर से लिखा जा सकता है:

DataTable<File, ProcessedFile> sharedMemory;

// in any thread
async Task WorkerAsync(...) {

  File f = await channel.ReadAsync();

  ProcessedFile p = await ProcessInParallelAsync(f);

  lock (_lock) { 
    sharedMemory.add(f, p);
  } 
}



वे क्या दिखते हैं ProcessInParallel? उदाहरण के लिए, हमारे पास एक फाइल है। सबसे पहले, हम इसे lexemes में तोड़ते हैं, और हमारे पास समानांतर में दो कार्य हो सकते हैं: खोज कैश का निर्माण और एक वाक्यविन्यास पेड़ का निर्माण। उसके बाद "शब्दार्थ त्रुटियों की खोज" का कार्य आता है। यहां यह महत्वपूर्ण है कि ये सभी कार्य एक निर्देशित चक्रीय ग्राफ बनाते हैं। यही है, आप कुछ हिस्सों को समानांतर थ्रेड्स में चला सकते हैं, कुछ नहीं कर सकते हैं, और स्पष्ट रूप से निर्भरताएं हैं जो कार्य अन्य कार्यों के लिए इंतजार करना चाहिए। आपको ऐसे कार्यों का एक ग्राफ मिलता है, आप किसी भी तरह उन्हें थ्रेड के साथ बिखेरना चाहते हैं। क्या त्रुटियों के बिना, इसे खूबसूरती से लिखना संभव है? हमारे कोड में, इस समस्या को कई बार हल किया गया था, हर बार एक अलग तरीके से। यह शायद ही कभी होता है जब यह कोड त्रुटियों के बिना लिखा जाता है।



हम इस कार्य ग्राफ़ को निम्नानुसार परिभाषित करते हैं: मान लें कि प्रत्येक कार्य में अन्य कार्य हैं, जिस पर यह निर्भर करता है, फिर ExecuteBefore शब्दकोश का उपयोग करते हुए हम अपनी विधि का कंकाल लिखते हैं।

कंकाल के उपाय


Dictionary<Action<ProcessedFile>, Action<ProcessedFile>[]> ExecuteBefore; async Task<ProcessedFile> ProcessInParallelAsync() {
  var res = new ProcessedFile();


  // lots of work with toposort, locks, etc.

  return res; 
}

यदि आप इस समस्या को सिर-पर हल करते हैं, तो आपको इस ग्राफ की एक टोपोलॉजिकल सॉर्टिंग करने की आवश्यकता है। फिर एक कार्य करें जिसमें कोई आश्रित कार्य नहीं है, इसे निष्पादित करें, एक ताला के नीचे संरचना का विश्लेषण करें, देखें कि किन कार्यों में कोई आश्रित नहीं है। भागो, उन्हें किसी तरह से बिखेरो Task Runnerहम इसे थोड़ा और अधिक कॉम्पैक्ट रूप से लिखते हैं: विभिन्न थ्रेड्स पर इस तरह के कार्यों के ग्राफ + निष्पादन का सामयिक छांटना।

असिंच आलसी


Dictionary<Action<ProcessedFile>, Action<ProcessedFile>[]> ExecuteBefore;
async Task<ProcessedFile> ProcessInParallelAsync() {
  var res = new ProcessedFile();
  var lazy = new Dictionary<Action<ProcessedFile>, Lazy<Task>>(); 
  foreach ((action, beforeList) in ExecuteBefore)
    lazy[action] = new Lazy<Task>(async () => 
    {
      await Task.WhenAll(beforeList.Select(b => lazy[b].Value)) 
      await Task.Yield();
      action(res);
}
  await Task.WhenAll(lazy.Values.Select(l => l.Value)) 
  return res;
}

नामक एक पैटर्न है Async Lazy। हम अपना निर्माण करते हैं ProcessedFileजिस पर विभिन्न कार्यों को निष्पादित किया जाना चाहिए। आइए एक शब्दकोश बनाएं: हम अपने प्रत्येक चरण (Action ProcessedFile) को किसी टास्क में या किसी और टास्क से आलसी के रूप में स्वरूपित करेंगे और मूल ग्राफ़ के साथ चलेंगे। चर में actionही कार्रवाई होगी , और इससे पहले कि - उन कार्यों में जो हमारे पहले किए जाने चाहिए। फिर बनाने Lazyसे action। हम टास्क में लिखते हैं await। इस प्रकार, हम उन सभी कार्यों की प्रतीक्षा कर रहे हैं जो इससे पहले पूरे होने चाहिए। पहलेवादी में, Lazyइस शब्दकोश में जो भी है उसे चुनें

कृपया ध्यान दें कि यहां कुछ भी सिंक्रोनाइज़ नहीं किया जाएगा, इसलिए यह कोड नहीं गिरेगा ItemNotFoundException in Dictionary। हम उन सभी कार्यों को अंजाम देते हैं जो हमारे द्वारा किए गए थे, कार्रवाई द्वारा खोज करनाLazy Taskफिर हम अपनी कार्रवाई को अंजाम देते हैं। अंत में, आपको बस प्रत्येक कार्य को शुरू करने के लिए कहने की आवश्यकता है, अन्यथा आप कभी नहीं जानते कि क्या कुछ शुरू नहीं हुआ है। इस मामले में, कुछ भी शुरू नहीं हुआ। इसका उपाय है। यह विधि 10 मिनट में लिखी गई है, यह बिल्कुल स्पष्ट है।

इस प्रकार, एसिंक्रोनस कोड ने हमारा निर्णय लिया, शुरू में इसने जटिल प्रतिस्पर्धी कोड के साथ कुछ स्क्रीन पर कब्जा कर लिया। यहां वह बिल्कुल सुसंगत है। मैं इसका उपयोग भी नहीं करता ConcurrentDictionary, मैं सामान्य रूप से उपयोग करता हूं Dictionary, क्योंकि हम इसे प्रतिस्पर्धात्मक रूप से कुछ भी नहीं लिखते हैं। एक सुसंगत, सुसंगत कोड है। हम async-s खूबसूरती से उपयोग करते हुए समानांतर कोड लिखने की समस्या को हल करते हैं , जिसका अर्थ है - बग के बिना।

ताले से छुटकारा पाएं


DataTable<File, ProcessedFile> sharedMemory;

// in any thread
async Task WorkerAsync(...) {

  File f = await channel.ReadAsync();

  ProcessedFile p = await ProcessInParallelAsync(f);

    lock (_lock) {
      sharedMemory.add(f, p);
   }
 }

क्या यह async और इन तालों में खींचने लायक है? अब सभी प्रकार के async ताले, async semaphores हैं, अर्थात, समकालिक और अतुल्यकालिक कोड में प्राइमिटिव्स का उपयोग करने का प्रयास है। यह अवधारणा गलत प्रतीत होती है, क्योंकि लॉक के साथ आप समानांतर निष्पादन से कुछ की रक्षा करते हैं। हमारा काम समानांतर निष्पादन को अनुक्रमिक में बदलना है, क्योंकि यह आसान है। और अगर यह आसान है, तो कम त्रुटियां हैं।

Channel<Pair<File, ProcessedFile>> output;
// in any thread
async Task WorkerAsync(...) {

  File f = await channel.ReadAsync();

  ProcessedFile p = await ProcessInParallelAsync(f);
  
  await output.WriteAsync(); 
}

हम कुछ चैनल बना सकते हैं और वहां कुछ फाइल और प्रोसेस्डफाइल डाल सकते हैं, और ReadAsyncकुछ अन्य प्रक्रिया इस चैनल को प्रोसेस करेगी, और यह क्रमिक रूप से करेगी। खुद को लॉक करें, संरचना की सुरक्षा के अलावा, अनिवार्य रूप से पहुंच को रैखिक करता है, एक जगह जहां लगातार सभी धागे समानांतर होते हैं। और हम इसे स्पष्ट रूप से चैनल के साथ बदल रहे हैं।



वास्तुकला इस प्रकार है: श्रमिक फाइलों को प्राप्त करते हैं inputऔर उन्हें प्रोसेसर में कहीं भेजते हैं, जो सब कुछ क्रमिक रूप से संसाधित करता है, कोई समानता नहीं है। कोड बहुत सरल दिखता है। मैं समझता हूं कि इस तरह से सब कुछ नहीं किया जा सकता है। ऐसा आर्किटेक्चर, जब आप डेटा पाइप का निर्माण कर सकते हैं, तो हमेशा काम नहीं करता है।



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

सारांश


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

यदि आपने सिंक्रोनस कोड लिखने की आदत विकसित कर ली है, और भले ही एसिंक्रोनस कोड सिंक्रोनस के समान है, तो वहां प्राइमिटिव को न खींचें, जिसका उपयोग आप सिंक्रोनस कोड में करने के लिए करते हैं async mutex। फ़ीड का उपयोग करें, यदि संभव हो, और अन्य संदेश प्राइमेटिंग पास कर रहे हैं

सीरियल कोड समानांतर की तुलना में सरल है। यदि आप अपनी वास्तुकला लिख ​​सकते हैं ताकि यह क्रमिक रूप से दिखे, बिना समानांतर कोड और लॉक किए, तो वास्तुकला को क्रमिक रूप से लिखें।

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

क्या पढ़ना है?



-10 . DotNext .

All Articles