DRY सिद्धांत का उल्लंघन करने से कैसे बचें जब आपके पास कोड के दोनों async और सिंक संस्करण हैं?


15

मैं एक ऐसे प्रोजेक्ट पर काम कर रहा हूं जिसमें एक ही लॉजिक / मेथड के async और सिंक वर्जन दोनों को सपोर्ट करने की जरूरत है। इसलिए उदाहरण के लिए मुझे इसकी आवश्यकता है:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

इन विधियों के लिए async और सिंक तर्क हर संबंध में समान हैं सिवाय इसके कि एक async है और दूसरा नहीं है। क्या इस तरह के परिदृश्य में DRY सिद्धांत का उल्लंघन करने से बचने का एक वैध तरीका है? मैंने देखा कि लोग कहते हैं कि आप एक async विधि पर GetAwaiter ()। क्या वह धागा सभी परिदृश्यों में सुरक्षित है? क्या ऐसा करने के लिए एक और बेहतर तरीका है, या क्या मैं तर्क की नकल करने के लिए मजबूर हूं?


1
क्या आप इसके बारे में थोड़ा और कह सकते हैं कि आप यहां क्या कर रहे हैं? विशेष रूप से, आप अपने अतुल्यकालिक विधि में अतुल्यकालिक कैसे प्राप्त कर रहे हैं? क्या उच्च-विलंबता कार्य CPU बाध्य है या IO बाध्य है?
एरिक लिपर्ट

"एक async है और दूसरा नहीं है" वास्तविक विधि के कोड या सिर्फ इंटरफ़ेस में अंतर है? (स्पष्ट रूप से सिर्फ इंटरफ़ेस के लिए आप return Task.FromResult(IsIt());)
एलेक्सी लेवेनकोव

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

@EricLippert मैंने आपको केवल यह बताने के लिए एक विशिष्ट डमी उदाहरण जोड़ा कि कैसे 2 कोड बेस इस तथ्य के अलावा समान हैं कि एक async है। आप आसानी से एक परिदृश्य की कल्पना कर सकते हैं जहां यह विधि बहुत अधिक जटिल है और लाइनों और कोड की रेखाओं को दोहराया जाना है।
मार्को

@AlexeiLevenkov अंतर वास्तव में कोड में है, मैंने इसे प्रदर्शित करने के लिए कुछ डमी कोड जोड़े।
मार्को

जवाबों:


15

आपने अपने प्रश्न में कई प्रश्न पूछे। मैं उन्हें थोड़ा अलग कर दूंगा जितना आपने किया था। लेकिन पहले मैं सीधे सवाल का जवाब दूं।

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

मुझे तोड़ दो कि क्यों है। हम इस प्रश्न से शुरू करेंगे:


मैंने देखा कि लोग कहते हैं कि आप GetAwaiter().GetResult()एक async विधि पर उपयोग कर सकते हैं और इसे अपने सिंक विधि से लागू कर सकते हैं? क्या वह धागा सभी परिदृश्यों में सुरक्षित है?

इस सवाल का मुद्दा यह है कि "क्या मैं सिंक्रोनस और असिंक्रोनस रास्तों को साझा कर सकता हूं? सिंक्रोनस पथ बनाकर बस एसिंक्रोनस वर्जन पर एक सिंक्रोनस प्रतीक्षा करें?"

मुझे इस बिंदु पर सुपर स्पष्ट होने दें क्योंकि यह महत्वपूर्ण है:

आप किसी व्यक्ति से किसी भी तरह की छेड़छाड़ को रोक सकते हैं

यह बहुत बुरी सलाह है। जब तक आपके पास यह सबूत न हो कि कार्य सामान्य रूप से या असामान्य रूप से पूरा हो गया है, तब तक अतुल्यकालिक कार्य से एक परिणाम प्राप्त करना बहुत खतरनाक है ।

यह बहुत बुरी सलाह है, इस कारण पर विचार करें। आप लॉन घास काटना चाहते हैं, लेकिन आपका लॉन घास काटने की मशीन ब्लेड टूट गया है। आप इस वर्कफ़्लो का पालन करने का निर्णय लेते हैं:

  • एक वेब साइट से एक नया ब्लेड ऑर्डर करें। यह एक उच्च-विलंबता, अतुल्यकालिक ऑपरेशन है।
  • समकालिक रूप से प्रतीक्षा करें - अर्थात, तब तक सोएं जब तक आपके हाथ में ब्लेड न हो
  • समय-समय पर मेलबॉक्स की जांच करें कि क्या ब्लेड आ गया है।
  • बॉक्स से ब्लेड निकालें। अब आपके हाथ में है।
  • मावर में ब्लेड स्थापित करें।
  • लॉन की घास काटो।

क्या होता है? आप हमेशा के लिए सोते हैं क्योंकि मेल की जांच का कार्य अब मेल के आने के बाद होने वाली किसी चीज़ पर आधारित होता है

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

यदि आप एक अतुल्यकालिक प्रतीक्षा करते हैं तो सब कुछ ठीक है! आप समय-समय पर मेल की जांच करते हैं, और जब आप प्रतीक्षा कर रहे होते हैं, तो आप एक सैंडविच बनाते हैं या अपने कर या जो भी करते हैं; जब आप प्रतीक्षा कर रहे हों तो आप काम करते रहें।

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

इस विषय पर आगे पढ़ने के लिए, देखें

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

स्टीफन वास्तविक दुनिया के परिदृश्य की तुलना में मुझे बेहतर बताते हैं।


अब चलो "अन्य दिशा" पर विचार करें। क्या हम एसिंक्रोनस संस्करण बनाकर कोड साझा कर सकते हैं?

यही कारण है संभवतः और वास्तव में शायद निम्नलिखित कारणों के लिए एक बुरा विचार,।

  • यह अक्षम है अगर सिंक्रोनस ऑपरेशन उच्च-विलंबता IO कार्य है। यह अनिवार्य रूप से एक कार्यकर्ता को काम पर रखता है और उस कर्मचारी को तब तक सोता है जब तक कि एक कार्य पूरा नहीं हो जाता। थ्रेड्स काफी महंगे हैं । वे डिफ़ॉल्ट रूप से पता स्थान के एक लाख बाइट्स का उपभोग करते हैं, वे समय लेते हैं, वे ऑपरेटिंग सिस्टम संसाधन लेते हैं; आप बेकार काम करने वाले एक धागे को जलाना नहीं चाहते हैं।

  • थ्रेड को सुरक्षित रखने के लिए सिंक्रोनस ऑपरेशन नहीं लिखा जा सकता है।

  • यह है एक और अधिक उचित तकनीक अगर उच्च विलंबता काम प्रोसेसर बाध्य है, लेकिन आप शायद बस एक कार्यकर्ता धागा करने के लिए इसे बंद हाथ करने के लिए नहीं चाहते हैं तो यह तो है। आप संभवत: कार्य समानांतर लाइब्रेरी का उपयोग करके इसे अधिक से अधिक CPUs के साथ समानांतर करने के लिए उपयोग कर सकते हैं, आप संभवतः रद्दीकरण तर्क चाहते हैं, और आप केवल तुल्यकालिक संस्करण वह सब नहीं कर सकते, क्योंकि तब यह पहले से ही अतुल्यकालिक संस्करण होगा

आगे की पढाई; फिर, स्टीफन इसे बहुत स्पष्ट रूप से बताते हैं:

टास्क का उपयोग क्यों नहीं करना चाहिए।

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

टास्क के लिए अधिक "करो और न करो" परिदृश्यों।

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


इसके बाद क्या होता है? कोड साझा करने की दोनों तकनीकें या तो गतिरोध या बड़ी अक्षमता को जन्म देती हैं। हम जिस निष्कर्ष पर पहुँचते हैं वह यह है कि आपको चुनाव करना चाहिए। क्या आप ऐसा प्रोग्राम चाहते हैं जो कुशल हो और सही हो और कॉल करने वाले को प्रसन्न करता हो, या क्या आप सिंक्रोनस और एसिंक्रोनस रास्तों के बीच कोड की एक छोटी मात्रा को डुप्लिकेट करके दर्ज किए गए कुछ कीस्ट्रोक्स को बचाना चाहते हैं? तुम दोनों मत जाओ, मुझे डर है।


क्या आप बता सकते हैं कि Task.Run(() => SynchronousMethod())async वर्जन में वापस आना ओपी के लिए वैसा क्यों नहीं है?
गिलाउम सस्दी

2
@GuillaumeSasdy: मूल पोस्टर ने इस सवाल का जवाब दिया है कि काम क्या है: यह IO बाध्य है। वर्कर थ्रेड पर IO बाउंड टास्क चलाना बेकार है!
एरिक लिपर्ट

ठीक है, मैं समझता हूँ। मुझे लगता है कि आप सही हैं, और @StriplingWarrior जवाब भी इसे और अधिक बताते हैं।
गिलाउम सस्दी

2
@Marko लगभग कोई भी वर्कअराउंड है जो थ्रेड को ब्लॉक करता है (हाई लोड के तहत) सभी थ्रेडपूल थ्रेड्स का उपभोग करता है (जो आमतौर पर async ऑपरेशंस को पूरा करने के लिए उपयोग किया जाता है)। परिणाम के रूप में सभी थ्रेड्स इंतजार कर रहे होंगे और कोई भी कोड के "एस्कॉन ऑपरेशन पूरा" भाग को चलाने के लिए उपलब्ध नहीं होगा। अधिकांश वर्कआर्ड कम लोड परिदृश्यों में ठीक होते हैं (क्योंकि थ्रेड के बहुत सारे 2-3 प्रत्येक एकल ऑपरेशन के हिस्से के रूप में अवरुद्ध होते हैं) ... और यदि आप गारंटी दे सकते हैं कि async ऑपरेशन का पूरा होना नए OS थ्रेड (थ्रेड पूल) पर चलता है यहां तक ​​कि सभी मामलों के लिए काम कर सकते हैं (आप उच्च कीमत का भुगतान करते हैं)
एलेक्सी लेवेनकोव

2
कभी-कभी "वर्कर थ्रेड पर केवल सिंक्रोनस वर्जन करने के अलावा" कोई दूसरा तरीका नहीं होता है। उदाहरण के लिए, कि कैसे Dns.GetHostEntryAsync.NET में कार्यान्वित किया जाता है, और FileStream.ReadAsyncकुछ प्रकार की फ़ाइलों के लिए। ओएस बस कोई async इंटरफ़ेस प्रदान नहीं करता है, इसलिए रनटाइम को नकली करना पड़ता है (और यह भाषा विशिष्ट नहीं है - कहते हैं, Erlang रनटाइम कार्यकर्ता प्रक्रियाओं के पूरे पेड़ को चलाता है , प्रत्येक के अंदर कई थ्रेड्स के साथ, गैर-अवरोधक डिस्क I / O और नाम प्रदान करने के लिए संकल्प)।
जोकर_वद

6

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

  1. एसिंक्रोनस और सिंक्रोनस कोड अक्सर मौलिक रूप से भिन्न होते हैं। अतुल्यकालिक कोड में आमतौर पर एक रद्दकरण टोकन शामिल होना चाहिए, उदाहरण के लिए। और अक्सर यह अलग-अलग तरीकों से कॉलिंग को समाप्त करता है (जैसा कि आपका उदाहरण Query()एक और QueryAsync()दूसरे में कॉल करता है ), या विभिन्न सेटिंग्स के साथ कनेक्शन स्थापित करना। इसलिए जब यह संरचनात्मक रूप से समान होता है, तब भी व्यवहार में अक्सर पर्याप्त अंतर होते हैं ताकि उन्हें अलग-अलग आवश्यकताओं के साथ अलग कोड के रूप में माना जा सके। फ़ाइल वर्ग में विधियों के Async और सिंक कार्यान्वयन के बीच अंतर पर ध्यान दें , उदाहरण के लिए: उन्हें एक ही कोड का उपयोग करने के लिए कोई प्रयास नहीं किया जाता है
  2. यदि आप एक इंटरफ़ेस लागू करने के लिए एक एसिंक्रोनस विधि हस्ताक्षर प्रदान कर रहे हैं, लेकिन आप एक तुल्यकालिक कार्यान्वयन (यानी वहाँ क्या आपके विधि के बारे में स्वाभाविक रूप से कुछ भी नहीं है) हो सकता है, तो आप बस वापस कर सकते हैं Task.FromResult(...)
  3. तर्क के किसी भी तुल्यकालिक टुकड़े जो दोनों विधियों के बीच समान हैं , उन्हें एक अलग सहायक विधि से निकाला जा सकता है और दोनों विधियों में लीवरेज किया जा सकता है।

सौभाग्य।


-2

आसान; सिंक्रोनस एक को कॉल करने के लिए एक async है। वहाँ भी एक आसान तरीका पर Task<T>ऐसा करने के लिए है:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}

1
मेरा उत्तर इस बात के लिए देखें कि यह सबसे खराब अभ्यास क्यों है जो आपको कभी नहीं करना चाहिए।
एरिक लिपर्ट

1
आपका उत्तर GetAwaiter () के साथ संबंधित है। GetResult ()। मैंने उससे परहेज किया। वास्तविकता यह है कि कभी-कभी आपको कॉल स्टैक में इसका उपयोग करने के लिए एक विधि को समकालिक रूप से चलाना पड़ता है जिसे एसिंक्स नहीं बनाया जा सकता है। यदि सिंक वेट कभी नहीं किया जाना चाहिए, तो C # टीम के सदस्य (पूर्व) के रूप में, कृपया मुझे बताएं कि आपने TPL में एक अतुल्यकालिक कार्य पर एक या दो नहीं बल्कि सिंक्रोनाइज़ करने के लिए एक तरीके से प्रतीक्षा की है।
कीथ

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