यह async कार्रवाई क्यों लटकी है?


102

मेरे पास एक बहु स्तरीय .Net 4.5 अनुप्रयोग है जो C # के नए asyncऔर awaitकीवर्ड का उपयोग करते हुए एक विधि को कॉल करता है जो बस लटका रहता है और मैं क्यों नहीं देख सकता।

नीचे मेरे पास एक async विधि है जो हमारे डेटाबेस उपयोगिता OurDBConn(मूल रूप से अंतर्निहित DBConnectionऔर DBCommandवस्तुओं के लिए एक आवरण ) का विस्तार करती है:

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

फिर मेरे पास एक मध्य स्तर की async विधि है जो कुछ धीमी गति से चलने वाले योग प्राप्त करने के लिए इसे कॉल करती है:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

अंत में मेरे पास UI विधि (MVC क्रिया) है जो समकालिक रूप से चलती है:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

समस्या यह है कि यह हमेशा के लिए उस अंतिम पंक्ति पर लटका रहता है। अगर मैं फोन करता हूं तो यह वही काम करता है asyncTask.Wait()। अगर मैं धीमी SQL विधि को सीधे चलाता हूं तो लगभग 4 सेकंड लगते हैं।

मैं जिस व्यवहार की उम्मीद कर रहा हूं, वह यह है कि जब यह हो जाए asyncTask.Result, अगर यह खत्म नहीं होता है तो इसे तब तक इंतजार करना चाहिए, और एक बार यह परिणाम वापस कर देना चाहिए।

अगर मैं डीबगर के साथ कदम रखता हूं तो SQL स्टेटमेंट पूरा हो जाता है और लैम्बडा फंक्शन खत्म हो जाता है, लेकिन return result;लाइन GetTotalAsyncकभी नहीं पहुंचती।

किसी भी विचार मैं गलत क्या कर रहा हूँ?

इसे ठीक करने के लिए मुझे कहाँ जाँच करने की आवश्यकता है?

क्या यह कहीं गतिरोध हो सकता है, और यदि ऐसा है तो इसे खोजने का कोई सीधा तरीका है?

जवाबों:


150

हाँ, यह सब एक गतिरोध है। और TPL के साथ एक सामान्य गलती है, तो बुरा मत मानना।

जब आप लिखते हैं await foo, तो रनटाइम, डिफ़ॉल्ट रूप से, उसी सिंक्रोनाइज़ेशन कॉन्टेक्स्ट पर फ़ंक्शन की निरंतरता को शेड्यूल करता है जो विधि शुरू हुई थी। अंग्रेजी में, मान लें कि आपने ExecuteAsyncUI थ्रेड से अपना कॉल किया है । आपकी क्वेरी थ्रेडपूल थ्रेड पर चलती है (क्योंकि आपने कॉल किया था Task.Run), लेकिन आप तब परिणाम का इंतजार करते हैं। इसका मतलब है कि रनटाइम return result;UI थ्रेड पर वापस चलाने के लिए आपकी " " पंक्ति को शेड्यूल करेगा , बजाय इसे थ्रेडपूल पर वापस शेड्यूल किए।

तो यह गतिरोध कैसे है? कल्पना कीजिए कि आपके पास बस यह कोड है:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

तो पहली पंक्ति अतुल्यकालिक काम बंद कर देती है। दूसरी पंक्ति तब UI थ्रेड को ब्लॉक करती है । इसलिए जब रनटाइम UI थ्रेड पर "रिटर्न रिजल्ट" लाइन को रन करना चाहता है, तो यह तब तक नहीं कर सकता है जब तक कि Resultपूर्ण न हो जाए। लेकिन निश्चित रूप से, परिणाम तब तक नहीं दिया जा सकता है जब तक कि वापसी नहीं होती है। गतिरोध।

यह TPL का उपयोग करने के एक प्रमुख नियम को दर्शाता है: जब आप .ResultUI थ्रेड (या कुछ अन्य फैंसी सिंक संदर्भ) पर उपयोग करते हैं, तो आपको यह सुनिश्चित करने के लिए सावधान रहना चाहिए कि टास्क पर निर्भर कुछ भी यूआई थ्रेड के लिए निर्धारित नहीं है। वरना बुराई होती है।

तो तुम क्या करते हो? विकल्प # 1 हर जगह प्रतीक्षा का उपयोग करता है, लेकिन जैसा कि आपने कहा कि पहले से ही एक विकल्प नहीं है। दूसरा विकल्प जो आपके लिए उपलब्ध है वह बस इंतजार का उपयोग बंद करना है। आप अपने दो कार्यों को फिर से लिख सकते हैं:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

क्या फर्क पड़ता है? अब कहीं भी इंतजार नहीं किया जा रहा है, इसलिए यूआई थ्रेड में निहित कुछ भी नहीं है। इन तरीकों के लिए जैसे कि एक ही रिटर्न है, एक " var result = await...; return result" पैटर्न करने का कोई मतलब नहीं है ; बस async संशोधक को हटा दें और सीधे कार्य ऑब्जेक्ट को सीधे पास करें। यह कम उपरि है, अगर कुछ और नहीं।

विकल्प # 3 यह निर्दिष्ट करना है कि आप अपने थ्रेड को यूआई थ्रेड पर वापस शेड्यूल नहीं करना चाहते हैं, लेकिन थ्रेड पूल पर शेड्यूल करें। आप इसे ConfigureAwaitविधि के साथ करते हैं , जैसे:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

यदि आप इस पर हैं, तो किसी कार्य को आम तौर पर UI थ्रेड के लिए शेड्यूल किया जाएगा; परिणाम का इंतजार करने से ContinueAwaitआप जिस भी संदर्भ में हैं, उसे अनदेखा करेंगे, और हमेशा थ्रेडपूल के लिए शेड्यूल करेंगे। इसका नकारात्मक पक्ष यह है कि आपको अपने सभी कार्यों में इसे हर जगह छिड़कना होगा। आप पर निर्भर करता है, क्योंकि कोई भी चूक .ConfigureAwaitदूसरे गतिरोध का कारण हो सकती है।


6
BTW, प्रश्न ASP.NET के बारे में है, इसलिए UI थ्रेड नहीं है। लेकिन ASP.NET की वजह से गतिरोध वाला मुद्दा बिल्कुल वैसा ही है SynchronizationContext
svick

मैंने बहुत कुछ समझाया, जैसा कि मेरे पास .Net 4 कोड था जिसमें समस्या नहीं थी लेकिन वह async/ awaitकीवर्ड्स के बिना TPL का उपयोग करता था ।
कीथ

2
TPL = टास्क पैरेलल
जेमी

अगर किसी को VB.net कोड की तलाश है (मेरी तरह) तो इसे यहाँ समझाया गया है: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue


36

यह क्लासिक मिश्रित asyncगतिरोध परिदृश्य है, जैसा कि मैं अपने ब्लॉग पर वर्णन करता हूं । जेसन ने इसे अच्छी तरह से वर्णित किया: डिफ़ॉल्ट रूप से, एक "संदर्भ" हर पर सहेजा जाता है awaitऔर asyncविधि जारी रखने के लिए उपयोग किया जाता है । यह "संदर्भ" वर्तमान है SynchronizationContextजब तक कि यह नहीं है null, जिस स्थिति में यह वर्तमान है TaskScheduler। जब asyncविधि जारी रखने का प्रयास करती है, तो यह पहले कैप्चर किए गए "संदर्भ" (इस मामले में, ASP.NET SynchronizationContext) में फिर से प्रवेश करती है । ASP.NET SynchronizationContextकेवल एक समय में संदर्भ में एक थ्रेड की अनुमति देता है, और संदर्भ में पहले से ही एक थ्रेड है - थ्रेड अवरुद्ध पर Task.Result

दो दिशानिर्देश हैं जो इस गतिरोध से बचेंगे:

  1. asyncनीचे सभी तरह का उपयोग करें । आप उल्लेख करते हैं कि आप ऐसा नहीं कर सकते, लेकिन मुझे यकीन नहीं है कि क्यों नहीं। .NET 4.5 पर ASP.NET MVC निश्चित रूप से asyncक्रियाओं का समर्थन कर सकता है , और इसे बनाने के लिए एक कठिन परिवर्तन नहीं है।
  2. ConfigureAwait(continueOnCapturedContext: false)जितना संभव हो उतना उपयोग करें । यह कैप्चर किए गए संदर्भ पर फिर से शुरू करने के डिफ़ॉल्ट व्यवहार को ओवरराइड करता है।

क्या ConfigureAwait(false)गारंटी है कि वर्तमान फ़ंक्शन एक अलग संदर्भ पर फिर से शुरू होता है?
च्यू x

MVC फ्रेमवर्क इसका समर्थन करता है, लेकिन यह मौजूदा MVC ऐप का हिस्सा है जिसमें बहुत सारे क्लाइंट साइड JS पहले से मौजूद हैं। asyncजिस तरह से यह क्लाइंट की तरफ से काम करता है, उसे तोड़े बिना मैं आसानी से एक्शन में नहीं आ सकता । मैं निश्चित रूप से उस विकल्प की जांच करने की योजना बनाता हूं, हालांकि लंबे समय तक।
कीथ

बस अपनी टिप्पणी को स्पष्ट करने के लिए - मैं उत्सुक था ConfigureAwait(false)कि क्या कॉल ट्री का उपयोग करने से ओपी की समस्या हल हो जाती।
च्यू x

3
@ कीथ: एमवीसी कार्रवाई asyncकरने से ग्राहक पक्ष प्रभावित नहीं होता है। मैं इसे एक अन्य ब्लॉग पोस्ट में समझाता हूं, asyncHTTP प्रोटोकॉल नहीं बदलता है
स्टीफन क्लीयर

1
@ कीथ: यह asyncकोडबेस के माध्यम से "बढ़ने" के लिए सामान्य है । यदि आपकी नियंत्रक पद्धति अतुल्यकालिक संचालन पर निर्भर हो सकती है, तो बेस क्लास विधि वापस आनी चाहिएTask<ActionResult> । एक बड़ी परियोजना को asyncबदलना हमेशा अजीब होता है क्योंकि मिश्रण asyncऔर सिंक कोड मुश्किल और मुश्किल होता है। शुद्ध asyncकोड ज्यादा सरल है।
स्टीफन क्लीरी

12

मैं उसी गतिरोध की स्थिति में था, लेकिन मेरे मामले में एक सिंक विधि से एक async विधि को कॉल करना, मेरे लिए क्या काम करता था:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

क्या यह एक अच्छा दृष्टिकोण है, कोई विचार है?


यह समाधान मेरे लिए भी काम कर रहा है, लेकिन मुझे यकीन नहीं है कि यह एक अच्छा समाधान है या यह कहीं टूट सकता है। कोई भी समझा सकता है कि
कोन्सटेंटिन वडोवकिन

अंत में मैं इस समाधान के साथ चला गया और यह बिना किसी परेशानी के एक उत्पादक वातावरण में काम कर रहा है .....
डैनिलो

1
मुझे लगता है कि आप Task.Run का उपयोग करके एक प्रदर्शन हिट ले रहे हैं। मेरे परीक्षण में Task.Run लगभग 100ms http अनुरोध के लिए निष्पादन समय को दोगुना कर रहा है।
टिमोथी गोंजालेज

1
यह समझ में आता है, आप एक async कॉल लपेटने के लिए एक नया कार्य बना रहे हैं, प्रदर्शन व्यापार है
Danilow

शानदार यह मेरे लिए भी काम करता है, मेरा मामला भी एक अतुल्यकालिक फोन एक तुल्यकालिक विधि के कारण था। धन्यवाद!
लियोनार्डो स्पाइना

4

बस स्वीकृत उत्तर (टिप्पणी करने के लिए पर्याप्त प्रतिनिधि नहीं) में जोड़ने के लिए, मेरे पास यह मुद्दा था जब उपयोग करते हुए अवरुद्ध करना task.Result, घटना हालांकि awaitइसके नीचे हर घटना थी ConfigureAwait(false), जैसा कि इस उदाहरण में है:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

मुद्दा वास्तव में बाहरी पुस्तकालय कोड के साथ है। Async लाइब्रेरी पद्धति ने कॉलिंग सिंक संदर्भ में जारी रखने की कोशिश की, चाहे मैं कैसे भी प्रतीक्षा को कॉन्फ़िगर कर रहा हूं, गतिरोध के कारण।

इस प्रकार, उत्तर बाहरी पुस्तकालय कोड के अपने संस्करण को रोल करने के लिए था ExternalLibraryStringAsync, ताकि इसमें वांछित निरंतरता गुण हों।


ऐतिहासिक उद्देश्यों के लिए गलत उत्तर

बहुत दर्द और पीड़ा के बाद, मैंने इस ब्लॉग पोस्ट में दफन समाधान ('गतिरोध' के लिए Ctrl-f) पाया। यह task.ContinueWithनंगे के बजाय, का उपयोग करके घूमता है task.Result

पहले का गतिरोध उदाहरण:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

इस तरह गतिरोध से बचें:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

के लिए क्या downvote है? यह समाधान मेरे लिए काम कर रहा है।
कैमरन जेफर्स

आप ऑब्जेक्ट Taskको पूरा होने से पहले लौटा रहे हैं , और कॉल करने वाले को यह निर्धारित करने का कोई साधन नहीं प्रदान करते हैं कि लौटी हुई वस्तु का म्यूटेशन वास्तव में कब होता है।
सेवाकाल

हम्म हाँ मैं देख रहा हूँ। तो क्या मुझे "प्रतीक्षा करें जब तक कि कार्य पूरा होने तक प्रतीक्षा करें" विधि जो लूप (या ऐसा कुछ) मैन्युअल रूप से अवरुद्ध का उपयोग करती है? या इस तरह के ब्लॉक को GetFooSynchronousविधि में पैक करें ?
कैमरन जेफर्स

1
यदि आप करते हैं, तो यह गतिरोध होगा। आपको Taskअवरुद्ध करने के बजाय वापस लौटने के लिए सभी तरह से सिंक करने की आवश्यकता है ।
सेवाकाल

दुर्भाग्य से यह एक विकल्प नहीं है, वर्ग एक तुल्यकालिक इंटरफ़ेस को लागू करता है जिसे मैं बदल नहीं सकता।
कैमरन जेफर्स

0

त्वरित उत्तर: इस लाइन को बदलें

ResultClass slowTotal = asyncTask.Result;

सेवा

ResultClass slowTotal = await asyncTask;

क्यों? कंसोल अनुप्रयोगों को छोड़कर अधिकांश अनुप्रयोगों के अंदर कार्यों के परिणाम प्राप्त करने के लिए आपको .result का उपयोग नहीं करना चाहिए यदि आप ऐसा करते हैं तो आपका कार्यक्रम वहां पहुंचने पर लटका होगा

यदि आप उपयोग करना चाहते हैं तो आप नीचे दिए गए कोड को भी आज़मा सकते हैं

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.