C # में async / प्रतीक्षा का आगमन सरल और सही समानांतर कोड लिखने के तरीके को पुनर्परिभाषित करने के लिए प्रेरित करता है। अक्सर, अतुल्यकालिक प्रोग्रामिंग का उपयोग करते हुए, प्रोग्रामर न केवल उन समस्याओं को हल करते हैं जो थ्रेड्स के साथ थे, बल्कि नए लोगों को भी पेश करते हैं। गतिरोध और उड़ानें कहीं भी नहीं जाती हैं - वे निदान करना कठिन हो जाता है।
दिमित्री इवानोव - हुआवेई में सॉफ्टवेयर एनालिसिस टीमलीड, जेटबाइन्स राइडर तकनीक के एक पूर्व और रिस्पेर कोर के डेवलपर: डेटा संरचनाएं, कैश, मल्टीथ्रेडिंग और डॉटनेट सम्मेलन में एक नियमित वक्ता ।कटस्कैन के तहत - डोटनेक्स्ट 2019 पाइटर सम्मेलन से दिमित्री की रिपोर्ट की वीडियो रिकॉर्डिंग और टेक्स्ट ट्रांसक्रिप्ट।वक्ता की ओर से आगे का कथन।बहु-थ्रेडेड या एसिंक्रोनस कोड में, कुछ अक्सर टूट जाता है। इसका कारण गतिरोध और दौड़ दोनों हो सकता है। एक नियम के रूप में, एक दौड़ एक हजार में से एक बार दुर्घटनाग्रस्त होती है, अक्सर स्थानीय रूप से नहीं, केवल एक बिल्ड सर्वर पर, और इसे पकड़ने में कई दिन लगते हैं। मुझे यकीन है कि कई लोगों के लिए यह एक परिचित स्थिति है।इसके अलावा, अनुभवी डेवलपर्स द्वारा भी अतुल्यकालिक कोड को देखते हुए, मैं खुद को यह सोचकर पाता हूं कि कुछ चीजों को तीन गुना कम और अधिक सही तरीके से लिखा जा सकता है।यह बताता है कि समस्या लोगों में नहीं है, लेकिन साधन में है। लोग सिर्फ उपकरण का उपयोग करते हैं और चाहते हैं कि यह उनकी समस्या का समाधान हो। इस टूल में बहुत बड़ी संख्या में क्षमताएं (कभी-कभी अति सूक्ष्म भी हैं), सेटिंग्स, एक अंतर्निहित संदर्भ, जो इस तथ्य की ओर जाता है कि गलत तरीके से उपयोग करना बहुत आसान है। आइए यह पता लगाने की कोशिश करें कि कैसे async का उपयोग करें / प्रतीक्षा करें और Task
.NET में एक वर्ग के साथ काम करें ।योजना
- एस्कॉन / वेट के साथ हल होने वाले एप्रोच की समस्याएं।
- विवादास्पद डिजाइन के उदाहरण।
- वास्तविक जीवन का एक कार्य जिसे हम अतुल्यकालिक रूप से हल करेंगे।
Async / प्रतीक्षा और समस्याओं को हल करने के लिए
हमें async / प्रतीक्षा की आवश्यकता क्यों है? मान लीजिए कि हमारे पास कोड है जो साझा की गई मेमोरी के साथ काम करता है।काम की शुरुआत में, हम अनुरोध को पढ़ते हैं, इस मामले में, अवरुद्ध कतार से फ़ाइल (उदाहरण के लिए, इंटरनेट या डिस्क से), Dequeue अवरोधन अनुरोध का उपयोग करके (अवरुद्ध अनुरोध उदाहरणों के साथ चित्रों में लाल रंग में चिह्नित किया जाएगा)।इस दृष्टिकोण को बहुत सारे थ्रेड्स की आवश्यकता होती है, और प्रत्येक थ्रेड को संसाधनों की आवश्यकता होती है, शेड्यूलर पर लोड बनाता है। लेकिन यह मुख्य समस्या नहीं है। मान लीजिए कि लोग ऑपरेटिंग सिस्टम को फिर से लिख सकते हैं ताकि ये सिस्टम एक लाख और लाख धागे दोनों का समर्थन करें। लेकिन मुख्य समस्या यह है कि कुछ धागे बस नहीं लिए जा सकते हैं। उदाहरण के लिए, आपके पास एक उपयोगकर्ता इंटरफ़ेस थ्रेड है। कोई सामान्य पर्याप्त यूआई फ्रेमवर्क नहीं हैं जहां डेटा तक पहुंच केवल एक धागे से नहीं होगी, फिर भी। UI थ्रेड को ब्लॉक नहीं किया जा सकता है। और इसे ब्लॉक नहीं करने के लिए, हमें एसिंक्रोनस कोड की आवश्यकता है।अब बात करते हैं दूसरे टास्क की। फ़ाइल को पढ़ने के बाद, इसे किसी तरह संसाधित करने की आवश्यकता है। हम इसे समानांतर में करेंगे।आप में से कई लोगों ने सुना है कि समानता अतुल्यकालिक के समान नहीं है। इस मामले में, सवाल उठता है: क्या एसिंक्रोनस समानांतर कोड को अधिक कॉम्पैक्ट, सुंदर और तेज लिखने में मदद कर सकता है?अंतिम कार्य साझा मेमोरी के साथ काम करना है। क्या हमें इस तंत्र को ताले, सिंक्रोनाइज़ेशन से एसिंक्रोनस कोड के साथ खींचने की आवश्यकता है, या क्या इससे किसी तरह बचा जा सकता है? क्या async / इसके साथ मदद का इंतजार कर सकता है ?Async / प्रतीक्षा का पथ
आइए दुनिया में सामान्य रूप से और .NET में असिंक्रोनस प्रोग्रामिंग के विकास को देखें।वापस कॉल करें
Void Foo(params, Action callback) {…}
Void OurMethod() {
…
Foo(params,() =>{
…
});
}
अतुल्यकालिक प्रोग्रामिंग कॉलबैक से शुरू हुई। यही है, पहले आपको कोड के कुछ हिस्से को सिंक्रोनाइज़ करने की जरूरत है, और दूसरे हिस्से को - एसिंक्रोनसली। उदाहरण के लिए, आप एक फ़ाइल से पढ़ते हैं, और जब डेटा तैयार हो जाता है, तो यह आपको किसी तरह वितरित किया जाएगा। इस अतुल्यकालिक भाग को कॉलबैक के रूप में पारित किया जाता है ।अधिक कॉलबैक
void Foo(params, Action callback) {...}
void Bar(Action callback) {...}
void Baz(Action callback) {...}
void OurMethod() {
...
Foo(params, () => {
...
Bar(() => {
Baz(() => {
});
});
});
}
इस प्रकार, एक कॉलबैक से आप एक और कॉलबैक रजिस्टर कर सकते हैं , जिसमें से आप तीसरे कॉलबैक को पंजीकृत कर सकते हैं, और अंत में यह सब कॉलबैक हेल में बदल जाता है ।
कॉलबैक: अपवाद
void Foo(params, Action onSuccess, Action onFailure) {...}
void OurMethod() {
...
Foo(params, () => {
...
},
() => {
...
});
}
अपवादों के साथ कैसे काम करें? उदाहरण के लिए, 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(() =>{
...
})
.ContinueWith(_ =>{
Foo();
})
.ContinueWith(_ =>{
Bar();
})
.ContinueWith(_ =>{
Baz();
})
}
संयोजन के लिए, कॉलबैक नरक को ध्यान में रखते हुए , यह कम से कम परिवर्तनों के साथ कोड को दोहराने की उपस्थिति के बावजूद अधिक रैखिक रूप में प्रकट हो सकता है। ऐसा लगता है कि कोड इस तरह से सुधार कर रहा है, लेकिन यहां भी नुकसान हैं।कार्य प्रारंभ करें और जारी रखें
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(() =>
{
Task.Factory.StartNew(() => {
})
})
.ContinueWith(...)
मापदंडों का उपयोग करने के लिए अन्य विकल्प हैं। उदाहरण के लिए, एक पैरेंट-चाइल्ड पदानुक्रम तब उत्पन्न होता है जब आप एक कार्य लॉन्च करते हैं और इसके तहत दूसरा चलाते हैं। इस मामले में, यदि आप लिखना ContinueWith
है, तो आप ContinueWith
के अंदर का शुभारंभ कार्य के लिए इंतजार नहीं होंगे।
आप लिखेंगे TaskCreationOptions.AttachedToParent
तो ContinueWith
इंतजार रहेगा। आप अपने उत्पादों में इस संपत्ति का उपयोग कर सकते हैं। मुझे लगता है कि हर कोई एक उदाहरण के साथ आ सकता है जिसमें कार्यों का एक पदानुक्रम है, कार्य के साथ सबटस्क के लिए इंतजार कर रहा है, और इसके उपशीर्षक के लिए सबस्कॉस्क। कहीं भी लिखने की आवश्यकता नहीं है WaitForChildren
, यह प्रतीक्षा असिंक्रोनस रूप से होती है। यही है, माता-पिता के कार्य का शरीर समाप्त हो जाता है, और उसके बाद माता-पिता के कार्य को पूर्ण नहीं माना जाता है, तब तक अपनी निरंतरता शुरू नहीं करता है जब तक कि बच्चे काम नहीं करते।Task.Factory.StartNew(() =>
{
Foo();
})
.ContinueWith(...)
void Foo() {
Task.Factory.StartNew(() => {
}, TaskCreationOptions.AttachedToParent);
}
एक ऐसी समस्या हो सकती है जिसमें कार्य को कहीं स्थानांतरित ThreadStatic
कर दिया जाता है, फिर आपके द्वारा शुरू की गई हर चीज AttachedToParent
को इस मूल कार्य में जोड़ दिया जाएगा, जो कि एक खतरे की घंटी है।Task.Factory.StartNew(() =>
{
Foo();
}, TaskCreationOptions.DenyChildAttach)
.ContinueWith(...)
void Foo() {
Task.Factory.StartNew(() => {
}, TaskCreationOptions.AttachedToParent);
}
दूसरी ओर, एक विकल्प है जो पिछले विकल्प को रद्द करता है DenyChildAttach
। ऐसा अनुप्रयोग अक्सर होता है।Task.Run(() =>
{
Foo();
})
.ContinueWith(...)
void Foo() {
Task.Factory.StartNew(() => {
}, 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() {
}
}
}
मान लीजिए कि आपके पास एक गहरा ढेर है। यह CancellationToken
एकमात्र स्पष्ट पैरामीटर है जिस पर हमने चर्चा की। यह बिल्कुल सभी पदानुक्रमों के माध्यम से हर जगह प्रसारित किया जाना चाहिए। मुझे क्या करना चाहिए, अगर एक गहरी पदानुक्रम की उपस्थिति में, आपको रिसेप्शन को फेंकने के लिए, अपने काम को कहीं सबसे कम स्तर पर रद्द करने की आवश्यकता है? ऐसी ही एक खास ट्रिक है जिसका हम इस्तेमाल करते हैं। उसे कहा जाता है AsyncLocal
।static AsyncLocal<Cancelation> asyncLocalCancellation;
Task.Factory.StartNew(() =>
{
asyncLocalCancellation.Set(cancellationToken)
Foo();
}, cancellationToken);
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)
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();
continuationCode();
}
एक चाल है Task.Yield
- यह एक विशेष कार्य है जो यह सुनिश्चित करता है कि इसका वेटर हमेशा आपके पास वापस नहीं आएगा IsCompleted
। तदनुसार, continuation
इसे इस स्थान पर सिंक्रोनस नहीं कहा जाएगा। UI थ्रेड के लिए, यह महत्वपूर्ण हो सकता है क्योंकि आप इस थ्रेड को बड़ी मात्रा में नहीं लेते हैं।
निरंतरता के लिए एक धागा कैसे चुनें? Async / प्रतीक्षित दर्शन यह है: आप एसिंक्रोनस कोड को सिंक्रोनस के समान लिखते हैं। यदि आपके पास एक थ्रेड पूल है , तो इससे आपको कोई फर्क नहीं पड़ता है - निरंतरता कोड को दूसरे थ्रेड पर निष्पादित किया जाएगा। इस बात की परवाह किए बिना कि InnerAsync
जब आप इंतजार कर रहे थे या नहीं तो यह पूरा हो गया है , आपको यूआई थ्रेड पर निष्पादित करने के लिए सब कुछ चाहिए।कार्य प्रतीक्षा के लिए तंत्र निम्नानुसार है: इसे लिया जाता है static
, इसे कहा जाता हैSynchronizationContext
और इससे निर्माण होता है TaskScheduler
। SynchronizationContext पोस्ट विधि के साथ एक चीज है, जो विधि के समान है 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();
}
इंटरनेट पर सलाह की एक पागल राशि है: यदि आपके पास गतिरोध है , तो कृपया अपने सभी कन्फिगरएविट कोड को स्मियर करें और सबकुछ ठीक हो जाएगा। यह गलत तरीका है। ConfigureAwait
उन मामलों में उपयोग किया जा सकता है जहां आप प्रदर्शन में सुधार करना चाहते हैं, या विधि के अंत में, कुछ पुस्तकालय विधियों में।गतिरोध
async Task MyFuncAsync() {
synchronousCode();
await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false);
continuationCode();
}
myFuncAsync().Wait()
यह एक क्लासिक गतिरोध है । UI थ्रेड पर, उन्होंने दस सेकंड इंतजार किया और किया Wait
। आपने जो किया है Wait
, उसके कारण इसे continuationCode
कभी लॉन्च नहीं किया जाएगा, Wait
इसलिए यह कभी वापस नहीं आएगा। यह सब बहुत शुरुआत में होता है।async Task OnBluttionClick() {
int v = Button.Text.ParseInt();
await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false);
Button.Text.Set((v+1).ToString());
}
myFuncAsync().Wait()
कल्पना कीजिए कि यह कुछ वास्तविक गतिविधि है। हमने बटन पर क्लिक किया, इसे लिया Button.ParseInt
, इंतजार किया , लिखा ConfigureAwait
हमने कहा: "कृपया हमारे UI स्ट्रीम को बंद न करें, जारी रखें।" समस्या यह है कि हम ConfigureAwait
UI थ्रेड पर निष्पादित होने के बाद दूसरा भाग भी चाहते हैं, क्योंकि यह प्रतीक्षा का दर्शन है । यही है, आपका एसिंक्रोनस कोड सिंक्रोनस कोड के समान दिखता है, और उसी संदर्भ में चलता है। इस मामले में, निश्चित रूप से, एक त्रुटि होगी। और इसके अलावा Button.Text.Set
किसी भी विधि कॉल की संख्या हो सकती है जो उनके संदर्भ को भी मानती है। इस स्थिति में क्या करना है? तुम यह केर सकते हो:async Task MyFuncAsync() {
synchronousCode();
await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false);
continuationCode();
}
PumpUntil(() => task.IsCompleted);
UI थ्रेड के साथ, आपको उन Wait
थ्रेड्स पर ऐसा करने पर प्रतिबंध लगाना होगा, जिनमें एक सामान्य संदेश कतार हो। करने Wait
या लिखने के बजाय ConfigureAwait
, आप संदेशों की इस कतार को पंप कर सकते हैं, और साथ ही, निरंतरता को भी पंप किया जाएगा। यदि आप सिंक्रोनस और एसिंक्रोनस कोड को नहीं मिला सकते हैं, तो आपको उन्हें मिश्रण नहीं करना चाहिए। लेकिन कभी-कभी इससे बचा नहीं जा सकता है।उदाहरण के लिए, आपके पास पुराना कोड है, और आपको उन्हें मिलाना होगा, फिर आप UI स्ट्रीम को पंप करेंगे। विजुअल स्टूडियो ने उम्मीदों पर यूआई थ्रेड को पंप किया, यह SynchronizationContext
थोड़ा बदल गया। यदि आप किसी पर WaitHandle में जाते हैं Wait
, तो जब आप लटकाते हैं, तो आपका UI स्ट्रीम पंप होता है। इस प्रकार, वे दौड़ के पक्ष में गतिरोध और दौड़ के बीच चयन करते हैं ।Pumpuntil- यह एक गैर-आदर्श एपीआई है, अर्थात, जब आप एक अनियंत्रित जगह में यादृच्छिक निरंतरता का प्रदर्शन करते हैं, तो बारीकियां हो सकती हैं। कोई और रास्ता नहीं है, दुर्भाग्य से। मिक्स सिंक्रोनस और एसिंक्रोनस कोड। यदि कुछ भी हो, तो पूरे राइडर को पुराने स्थानों में व्यवस्थित किया जाता है, इसलिए कभी-कभी बारीकियां भी होती हैं।संदर्भ बदलें
async Task MyFuncAsync() {
synchronousCode();
await myTaskScheduler;
continuationCode();
}
Async / wait
का उपयोग करने का एक और दिलचस्प तरीका है । आप लिख सकते हैं Awaiter
पर scheduler
और धागे पर कूद। मैंने विजुअल स्टूडियो में पोस्ट पढ़ी, उन्होंने बहुत लंबे समय तक लिखा कि विधि के बीच में आगे और पीछे कूदना अच्छा नहीं है, लेकिन अब वे इसे स्वयं करते हैं। विजुअल स्टूडियो में एक एपीआई है जो शेड्यूलर्स के माध्यम से थ्रेड पर कूदता है। सामान्य उपयोग के लिए, यह करना अच्छा नहीं है।संरचित संगति
async Task MyFuncAsync() {
synchronousCode();
await Task.Factory.StartNew(() => {...}, myTaskScheduler);
continuationCode();
}
नए संदर्भ में सुविधाजनक विसर्जन के लिए और पुराने पर लौटने के लिए, कुछ संरचनात्मक प्रतियोगिता या संरचनात्मक समानता का निर्माण किया जाना चाहिए। उदाहरण के लिए, साठ के दशक में, 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;
}
पहले, अनहैन्ड एक्जीक्यूशन ने बस प्रक्रिया को फेंक दिया, और अगर आपने कुछ निष्पादन को नहीं पकड़ा UnobservedExceptionHandler
(यह कुछ ऐसा भी है static
जिसे आप अनुसूचियों से जोड़ सकते हैं), तो यह प्रक्रिया निष्पादित नहीं हुई। अब यह बिल्कुल वैध कोड है। हालाँकि .NET ने अपने व्यवहार को बदल दिया, लेकिन उसने विपरीत दिशा में व्यवहार को वापस करने के लिए सेटिंग को बनाए रखा।async Task MyAsync(CancellationToken cancellationToken) {
await SomeTask1 Async(cancellationToken);
await Some Task2Async( cancellation Token);
}
try {
await MyAsync( cancellation Token);
} catch (OperationException e) {
} catch (Exception e) {
log.Error(e);
}
देखें कि निष्पादन की प्रक्रिया कैसे चलती है। रद्दीकरण टोकेन-एस को प्रेषित किया जाना चाहिए, सभी कोड को रद्द करना "रद्द करना" आवश्यक है। Async का सामान्य व्यवहार यह है कि आप कहीं भी जाँच नहीं करते हैं Task.Status ancellationToken
, आप एसिंक्रोनस कोड के साथ उसी तरह काम करते हैं जैसे कि सिंक्रोनस के साथ। यही है, रद्दीकरण के मामले में, आपको एक निष्पादन मिलता है, और इस मामले में, जब आप इसे प्राप्त करते हैं तो आप कुछ भी नहीं करते हैं OperationCanceledException
।रद्द और दोषपूर्ण की स्थिति के बीच अंतर यह है कि आपको प्राप्त नहीं हुआ OperationCanceledException
, लेकिन सामान्य निष्पादन। और इस मामले में, हम इसे प्रतिज्ञा कर सकते हैं, आपको बस एक निष्पादन प्राप्त करने और इसके आधार पर निष्कर्ष निकालने की आवश्यकता है। यदि आप कार्य को स्पष्ट रूप से शुरू करते हैं, तो टास्क के माध्यम से, आप उड़ गए होंगे AggregateException
। और async में, मामले में वे AggregateException
हमेशा बहुत पहले निष्पादन को फेंक देते हैं जो इसमें था (इस मामले में - OperationCanceled
)।प्रयोग में
सिंक्रोनस विधि
DataTable<File, ProcessedFile> sharedMemory;
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;
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;
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();
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;
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;
async Task WorkerAsync(...) {
File f = await channel.ReadAsync();
ProcessedFile p = await ProcessInParallelAsync(f);
await output.WriteAsync();
}
हम कुछ चैनल बना सकते हैं और वहां कुछ फाइल और प्रोसेस्डफाइल डाल सकते हैं, और ReadAsync
कुछ अन्य प्रक्रिया इस चैनल को प्रोसेस करेगी, और यह क्रमिक रूप से करेगी। खुद को लॉक करें, संरचना की सुरक्षा के अलावा, अनिवार्य रूप से पहुंच को रैखिक करता है, एक जगह जहां लगातार सभी धागे समानांतर होते हैं। और हम इसे स्पष्ट रूप से चैनल के साथ बदल रहे हैं।
वास्तुकला इस प्रकार है: श्रमिक फाइलों को प्राप्त करते हैं input
और उन्हें प्रोसेसर में कहीं भेजते हैं, जो सब कुछ क्रमिक रूप से संसाधित करता है, कोई समानता नहीं है। कोड बहुत सरल दिखता है। मैं समझता हूं कि इस तरह से सब कुछ नहीं किया जा सकता है। ऐसा आर्किटेक्चर, जब आप डेटा पाइप का निर्माण कर सकते हैं, तो हमेशा काम नहीं करता है।
यह हो सकता है कि आपके पास एक दूसरा चैनल है जो आपके प्रोसेसर में आता है और चैनलों से नहीं, बल्कि चक्रीय निर्देशित ग्राफ बनता है, लेकिन चक्रों का एक ग्राफ। यह एक उदाहरण है कि रोमन एलिसारोव ने 2018 में कोटलिनकोफ को बताया। उन्होंने इन चैनलों के साथ कोटलिन पर एक उदाहरण लिखा था, और वहां चक्र थे, और इस उदाहरण को बंद कर दिया गया था। समस्या यह थी कि यदि आपके पास ग्राफ़ में ऐसे चक्र हैं, तो अतुल्यकालिक दुनिया में सब कुछ अधिक जटिल हो जाता है। एसिंक्रोनस डेडलॉक खराब हैं कि जब आप थ्रेड्स का एक स्टैक होता है, तो सिंक्रोनस को हल करना अधिक कठिन होता है, और यह स्पष्ट है कि क्या लटका हुआ है। इसलिए, यह एक उपकरण है जिसका सही उपयोग किया जाना चाहिए।सारांश
- अतुल्यकालिक कोड में सिंक्रनाइज़ेशन से बचें।
- सीरियल कोड समानांतर की तुलना में सरल है।
- एसिंक्रोनस कोड सरल हो सकता है और न्यूनतम मापदंडों और एक अंतर्निहित संदर्भ का उपयोग कर सकता है जो इसके व्यवहार को बदलते हैं।
यदि आपने सिंक्रोनस कोड लिखने की आदत विकसित कर ली है, और भले ही एसिंक्रोनस कोड सिंक्रोनस के समान है, तो वहां प्राइमिटिव को न खींचें, जिसका उपयोग आप सिंक्रोनस कोड में करने के लिए करते हैं async mutex
। फ़ीड का उपयोग करें, यदि संभव हो, और अन्य संदेश प्राइमेटिंग पास कर रहे हैं ।सीरियल कोड समानांतर की तुलना में सरल है। यदि आप अपनी वास्तुकला लिख सकते हैं ताकि यह क्रमिक रूप से दिखे, बिना समानांतर कोड और लॉक किए, तो वास्तुकला को क्रमिक रूप से लिखें।और आखिरी चीज जिसे हमने कार्यों के साथ बड़ी संख्या में उदाहरणों से देखा। जब आप अपने सिस्टम को डिज़ाइन करते हैं, तो अंतर्निहित संदर्भ पर कम भरोसा करने की कोशिश करें। अनुकरणीय संदर्भ कोड में क्या हो रहा है की गलतफहमी की ओर जाता है, और आप एक वर्ष में अंतर्निहित समस्याओं के बारे में भूल सकते हैं। और अगर कोई अन्य व्यक्ति इस कोड पर काम करता है और उसमें कुछ फिर से करता है, तो यह उन कठिनाइयों को जन्म दे सकता है, जिनके बारे में आपको एक बार पता था, और नए प्रोग्रामर को निहित संदर्भ के कारण नहीं पता है। नतीजतन, खराब डिजाइन को बड़ी संख्या में मापदंडों, उनके संयोजन और अंतर्निहित संदर्भ की विशेषता है।क्या पढ़ना है?
-10 . DotNext .