C# में सिंक्रोनस से एसिंक्रोनस विधि को कैसे कॉल करें? सारगर्भित, बेकार बात नहीं
\n\nकल्पना करो: तुम शाम को कोड पर बैठे हो, C# में एक पुरानी कंसोल उपयोगिता को ठीक कर रहे हो। सब कुछ सिंक्रोनस रूप से काम कर रहा है, घड़ी की तरह। और फिर — धम! — तुम्हें एक API को हिट करने की ज़रूरत है, जो केवल async Task के माध्यम से बनाया गया है। क्या पूरे प्रोग्राम को फिर से लिखना है? परिचित दर्द, है ना?
प्रश्न «How to call asynchronous method from synchronous method in C#» Stack Overflow पर सबसे लोकप्रिय में से एक है। और यह व्यर्थ नहीं है। वहाँ, टिप्पणियों में, «.Result करो और चिंता मत करो» से लेकर «क्या तुम पागल हो, तुम्हें डेडलॉक होगा!» तक ढेरों सलाह हैं। आइए इसे स्पष्ट रूप से समझते हैं: कौन से दृष्टिकोण वास्तव में काम करते हैं और कौन से नरक का रास्ता हैं।
\n\nसिर्फ «await» काम क्यों नहीं करता?
\n\nअगर तुम लिखने की कोशिश करोगे:
\npublic void SyncMethod()\n{\n await DoSomethingAsync(); // त्रुटि CS4032\n}\nकंपाइलर इसे कंपाइल करने से मना कर देगा। await का उपयोग केवल async संशोधक से चिह्नित विधि के अंदर ही किया जा सकता है। और async void — यह एक अलग कहानी है, जिसे हम भी समझेंगे।
तो, हमारे पास एक एसिंक्रोनस विधि है:
\npublic async Task<string> GetDataAsync()\n{\n using var httpClient = new HttpClient();\n return await httpClient.GetStringAsync("https://api.example.com/data");\n}\n\nऔर एक सिंक्रोनस विधि है, जिसमें से हम इसे कॉल करना चाहते हैं। आइए विकल्पों पर चलते हैं।
\n\nदृष्टिकोण 1: GetAwaiter().GetResult() — हैकर का आकर्षण
\n\nसबसे लोकप्रिय और, शायद, सबसे सुरुचिपूर्ण तरीका:
\npublic string CallSync()\n{\n return GetDataAsync().GetAwaiter().GetResult();\n}\n\nयह क्यों अच्छा है:
\n- \n
AggregateExceptionअपवाद उत्पन्न नहीं करता (.Resultके विपरीत)। यदि एसिंक्रोनस विधि के अंदर कोई त्रुटि होती है, तो तुम्हें उसका मूल प्रकार मिलेगा, न कि कोई रैपर। \n - कोड सामान्य रूप से पढ़ा जाता है, बिना अनावश्यक try-catch के। \n
लेकिन एक बारीकी है: यदि तुम्हारी सिंक्रोनस विधि SynchronizationContext वाले संदर्भ में निष्पादित होती है (उदाहरण के लिए, WinForms या WPF के UI थ्रेड में), तो तुम्हें डेडलॉक — थ्रेड्स का पारस्परिक अवरोध — होने का जोखिम है। थ्रेड A एसिंक्रोनस कार्य के पूरा होने की प्रतीक्षा कर रहा है, और कार्य पूरा नहीं हो सकता क्योंकि वह थ्रेड A के मुक्त होने की प्रतीक्षा कर रहा है। बकवास।
कैसे बचें: एसिंक्रोनस विधि में ही .ConfigureAwait(false) का उपयोग करें। लेकिन यह पहले से ही API को बदल रहा है, न कि केवल कॉल करने वाले कोड को।
दृष्टिकोण 2: Task.Run(async () => await ...).GetAwaiter() — थ्रेड पूल में निकासी
\n\nयदि तुम एसिंक्रोनस विधि को बदल नहीं सकते, लेकिन डेडलॉक से गारंटीकृत बचना चाहते हो — तो कार्य को थ्रेड पूल में ले जाओ:
\npublic string CallSyncSafe()\n{\n return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();\n}\n\nजादू: Task.Run डेलिगेट को ThreadPool के एक थ्रेड पर चलाता है, जिसमें SynchronizationContext नहीं होता। डेडलॉक असंभव है। भले ही मूल विधि ConfigureAwait(true) का उपयोग करती हो।
नुकसान: एक अतिरिक्त संदर्भ स्विच (context switch)। सैकड़ों कॉल के लिए — ठीक नहीं। एक बार के लिए — आदर्श।
\n\nदृष्टिकोण 3: .Result या .Wait() — दादी का तरीका
\n\npublic string CallSyncOldSchool()\n{\n return GetDataAsync().Result; // या .Wait()\n}\n\nक्या बुरा है:
\n- \n
- त्रुटि पर
AggregateExceptionफेंकता है। वास्तविक त्रुटि तक पहुँचने के लिए,InnerExceptionमें खोदना पड़ता है। \n - UI संदर्भ में डेडलॉक का वही जोखिम। \n
- कोड 2012 के क्रच जैसा दिखता है। \n
यह दृष्टिकोण कोड-रिव्यू के लिए लाल झंडा है। अगर मैं पुल-रिक्वेस्ट में ऐसा देखूँगा — तो रिजेक्ट कर दूँगा।
\n\nदृष्टिकोण 4: AsyncHelper या कस्टम RunSync
\n\nअनुभवी डेवलपर्स कभी-कभी अपने हेल्पर लिखते हैं। उदाहरण के लिए, ऐसा:
\n