C # 5 async CTP: EndAwait कॉल से पहले उत्पन्न कोड में आंतरिक "स्थिति" 0 पर सेट क्यों है?


195

कल मैं नए C # "async" फ़ीचर के बारे में बात कर रहा था, विशेष रूप से इस बात पर कि क्या उत्पन्न कोड जैसा दिख रहा है, और the GetAwaiter()/ BeginAwait()/ EndAwait()कॉल।

हमने C # कंपाइलर द्वारा उत्पन्न राज्य मशीन में कुछ विस्तार से देखा, और दो पहलू थे जिन्हें हम समझ नहीं पाए:

  • क्यों उत्पन्न वर्ग में एक Dispose()विधि और एक $__disposingचर होता है, जिसका उपयोग कभी नहीं होता है (और वर्ग लागू नहीं होता है IDisposable)।
  • आंतरिक stateचर को किसी भी कॉल से पहले 0 पर सेट क्यों किया जाता है EndAwait(), जब 0 सामान्य रूप से "यह प्रारंभिक प्रवेश बिंदु है" का अर्थ प्रतीत होता है।

मुझे संदेह है कि पहले बिंदु का जवाब कुछ और दिलचस्प तरीके से दिया जा सकता है, क्योंकि अगर किसी के पास कोई और जानकारी हो तो उसे सुनकर मुझे खुशी होगी। हालांकि यह सवाल दूसरे बिंदु के बारे में अधिक है।

यहाँ नमूना कोड का एक बहुत ही सरल टुकड़ा है:

using System.Threading.Tasks;

class Test
{
    static async Task<int> Sum(Task<int> t1, Task<int> t2)
    {
        return await t1 + await t2;
    }
}

... और यहां वह कोड है जो उस MoveNext()विधि के लिए उत्पन्न होता है जो राज्य मशीन को लागू करता है। इसे सीधे प्रतिक्षेपक से कॉपी किया जाता है - मैंने अकथ्य चर नाम तय नहीं किए हैं:

public void MoveNext()
{
    try
    {
        this.$__doFinallyBodies = true;
        switch (this.<>1__state)
        {
            case 1:
                break;

            case 2:
                goto Label_00DA;

            case -1:
                return;

            default:
                this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
                this.<>1__state = 1;
                this.$__doFinallyBodies = false;
                if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
                {
                    return;
                }
                this.$__doFinallyBodies = true;
                break;
        }
        this.<>1__state = 0;
        this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
        this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
        this.<>1__state = 2;
        this.$__doFinallyBodies = false;
        if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
        {
            return;
        }
        this.$__doFinallyBodies = true;
    Label_00DA:
        this.<>1__state = 0;
        this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
        this.<>1__state = -1;
        this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
    }
    catch (Exception exception)
    {
        this.<>1__state = -1;
        this.$builder.SetException(exception);
    }
}

यह लंबा है, लेकिन इस प्रश्न के लिए महत्वपूर्ण लाइनें ये हैं:

// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();

दोनों मामलों में राज्य को बाद में फिर से बदल दिया जाता है इससे पहले कि यह स्पष्ट रूप से मनाया जाता है ... तो इसे 0 पर क्यों सेट करें? यदि MoveNext()इस बिंदु पर फिर से बुलाया गया (सीधे या इसके माध्यम से Dispose) तो यह प्रभावी रूप से async विधि को फिर से शुरू करेगा, जो पूरी तरह से अनुपयुक्त होगा जहां तक ​​मैं बता सकता हूं ... अगर और MoveNext() नहीं बुलाया गया है, तो राज्य में परिवर्तन अप्रासंगिक है।

क्या यह बस कंपाइलर पुन: उपयोग करने वाले कंपाइलर ब्लॉक जेनरेशन कोड का एक साइड-इफ़ेक्ट है async के लिए, जहाँ इसका अधिक स्पष्ट स्पष्टीकरण हो सकता है?

महत्वपूर्ण अस्वीकरण

जाहिर है कि यह सिर्फ CTP कंपाइलर है। मैं पूरी तरह से चीजों को अंतिम रिलीज से पहले बदलने की उम्मीद करता हूं - और संभवतः अगले सीटीपी रिलीज से पहले भी। यह प्रश्न किसी भी तरह से यह दावा करने की कोशिश नहीं कर रहा है कि यह C # संकलक या उस जैसी किसी चीज़ में कोई दोष है। मैं सिर्फ यह जानने की कोशिश कर रहा हूं कि क्या इसके लिए कोई सूक्ष्म कारण है कि मैं चूक गया हूं :)


7
VB कंपाइलर एक समान राज्य मशीन का उत्पादन करता है (यह नहीं जानता कि यह अपेक्षित है या नहीं, लेकिन VB के पहले
इट्रेटर

1
@Rune: MoveNextDelegate सिर्फ एक प्रतिनिधि क्षेत्र है जो MoveNext को संदर्भित करता है। मेरा मानना ​​है कि हर बार वेटर में जाने के लिए एक नया एक्शन बनाने से बचने के लिए इसे कैश किया जाता है।
जॉन स्कीट

5
मुझे लगता है कि उत्तर है: यह एक CTP है। टीम के लिए उच्च क्रम बिट वहाँ से बाहर हो रहा था और भाषा डिजाइन मान्य था। और उन्होंने इतनी जल्दी कमाल कर दिया। आप काफी अलग करने के लिए भेज दिया कार्यान्वयन (संकलक के, MoveNext नहीं) की अपेक्षा करनी चाहिए। मुझे लगता है कि एरिक या लुसियान को उन लाइनों के साथ एक उत्तर के साथ वापस आना है जो यहां कुछ भी गहरा नहीं है, बस एक व्यवहार / बग जो ज्यादातर मामलों में कोई फर्क नहीं पड़ता और किसी ने भी ध्यान नहीं दिया। क्योंकि यह एक सीटीपी है।
क्रिस फेयर

2
@ स्टिलगर: मैंने सिर्फ ildasm से जाँच की है, और यह वास्तव में ऐसा कर रहा है।
जॉन स्कीट

3
@JonSkeet: ध्यान दें कि कोई भी उत्तरों को कैसे बढ़ाता है। यदि हम उत्तर को सही भी मानते हैं तो 99% वास्तव में नहीं बता सकते हैं।
the_drow

जवाबों:


71

ठीक है, मैं अंत में एक असली जवाब है। मैंने इसे अपने तरीके से काम किया, लेकिन टीम के वीबी हिस्से से केवल लुसियन विस्किक ने पुष्टि की कि वास्तव में इसके लिए एक अच्छा कारण है। उसके लिए बहुत धन्यवाद - और कृपया अपने ब्लॉग पर जाएँ , जो चट्टानों।

यहां मान 0 केवल विशेष है क्योंकि यह एक मान्य स्थिति नहीं है, जो कि आप awaitएक सामान्य मामले में पहले हो सकते हैं । विशेष रूप से, यह एक राज्य नहीं है जो राज्य मशीन कहीं और के लिए परीक्षण समाप्त कर सकती है। मेरा मानना ​​है कि किसी भी गैर-सकारात्मक मूल्य का उपयोग करना बस के रूप में अच्छी तरह से काम करेगा: -1 इसके लिए उपयोग नहीं किया जाता है क्योंकि यह तार्किक रूप से गलत है, क्योंकि -1 का अर्थ है "समाप्त"। मैं तर्क दे सकता हूं कि हम वर्तमान में 0 पर एक अतिरिक्त अर्थ दे रहे हैं, लेकिन अंततः यह वास्तव में मायने नहीं रखता है। इस प्रश्न का बिंदु यह पता लगा रहा था कि राज्य को क्यों सेट किया जा रहा है।

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

यहाँ इस async विधि है:

static async Task<int> FooAsync()
{
    var t = new SimpleAwaitable();

    for (int i = 0; i < 3; i++)
    {
        try
        {
            Console.WriteLine("In Try");
            return await t;
        }                
        catch (Exception)
        {
            Console.WriteLine("Trying again...");
        }
    }
    return 0;
}

वैचारिक रूप से, SimpleAwaitableकिसी भी प्रतीक्षा योग्य हो सकता है - शायद एक कार्य, शायद कुछ और। मेरे परीक्षणों के प्रयोजनों के लिए, यह हमेशा के लिए गलत हो जाता है IsCompleted, और इसमें अपवाद फेंकता है GetResult

इसके लिए जनरेट कोड है MoveNext:

public void MoveNext()
{
    int returnValue;
    try
    {
        int num3 = state;
        if (num3 == 1)
        {
            goto Label_ContinuationPoint;
        }
        if (state == -1)
        {
            return;
        }
        t = new SimpleAwaitable();
        i = 0;
      Label_ContinuationPoint:
        while (i < 3)
        {
            // Label_ContinuationPoint: should be here
            try
            {
                num3 = state;
                if (num3 != 1)
                {
                    Console.WriteLine("In Try");
                    awaiter = t.GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        state = 1;
                        awaiter.OnCompleted(MoveNextDelegate);
                        return;
                    }
                }
                else
                {
                    state = 0;
                }
                int result = awaiter.GetResult();
                awaiter = null;
                returnValue = result;
                goto Label_ReturnStatement;
            }
            catch (Exception)
            {
                Console.WriteLine("Trying again...");
            }
            i++;
        }
        returnValue = 0;
    }
    catch (Exception exception)
    {
        state = -1;
        Builder.SetException(exception);
        return;
    }
  Label_ReturnStatement:
    state = -1;
    Builder.SetResult(returnValue);
}

मुझे Label_ContinuationPointइसे वैध कोड बनाने के लिए आगे बढ़ना पड़ा - अन्यथा यह gotoबयान के दायरे में नहीं है - लेकिन यह उत्तर को प्रभावित नहीं करता है।

GetResultइसके अपवाद को फेंकने पर क्या होता है, इसके बारे में सोचें । हम कैच ब्लॉक, वेतन वृद्धि i, और फिर लूप राउंड से गुजरेंगे (यह मानते हुए iअभी भी 3 से कम है)। हम अभी भी उस स्थिति में हैं GetResultजब हम कॉल करने से पहले थे ... लेकिन जब हम tryब्लॉक के अंदर पहुंचते हैं तो हमें "इन ट्राई" प्रिंट करना चाहिए और GetAwaiterफिर से कॉल करना चाहिए ... और हम केवल यही करेंगे कि यदि राज्य 1. नहीं है। state = 0काम, यह मौजूदा awaiter का उपयोग करें और छोड़ देगा Console.WriteLineकॉल।

यह कोड के माध्यम से काम करने के लिए एक बहुत ही दर्दनाक बिट है, लेकिन यह सिर्फ उस प्रकार की चीज को दिखाने के लिए जाता है जिसके बारे में टीम को सोचना है। मुझे खुशी है कि मैं इसे लागू करने के लिए जिम्मेदार नहीं हूं :)


8
@ शेखर_प्रो: हाँ, यह एक गोटो है। तुम्हे करना चाहिए ऑटो-जनरेटेड स्टेट मशीनों में बहुत सारे गोटो स्टेटमेंट देखने की उम्मीद :)
जॉन स्केट

12
@ शेखर_प्रो: मैन्युअल रूप से लिखे गए कोड के भीतर, यह है - क्योंकि यह कोड को पढ़ने और अनुसरण करने के लिए कठिन बनाता है। हालांकि, मेरे जैसे मूर्खों को छोड़कर कोई भी ऑटोगेनेरेटेड कोड नहीं पढ़ता है :)
जॉन स्कीट

तो क्या होता है जब हम एक अपवाद के बाद फिर से इंतजार करते हैं? हम फिर से शुरू करते हैं?
विन्यासकर्ता

1
@configurator: यह गेटएवाटर को वेटिटेबल पर बुलाता है, जो कि मुझे इसकी उम्मीद है।
जॉन स्कीट

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

5

अगर इसे 1 (पहले मामले) में रखा गया था, तो आपको कॉल करने के लिए EndAwaitबिना कॉल मिलेगा BeginAwait। यदि इसे 2 (दूसरे मामले) में रखा गया है, तो आपको दूसरे वेटर पर समान परिणाम मिलेगा।

मैं अनुमान लगा रहा हूं कि बिगिनिट को कॉल करना झूठा है अगर यह पहले से ही शुरू हो गया है (मेरी तरफ से अनुमान) और एंडवाइट पर लौटने के लिए मूल मूल्य रखता है। यदि ऐसा है तो यह सही ढंग से काम करेगा जबकि यदि आप इसे -1 पर सेट करते हैं तो आप this.<1>t__$await1पहले मामले के लिए एक अनइंस्टालिज्ड हो सकते हैं ।

हालाँकि यह मानता है कि BeginAwaiter वास्तव में पहले के बाद किसी भी कॉल पर कार्रवाई शुरू नहीं करेगा और यह उन मामलों में गलत वापस आएगा। शुरू करना अस्वीकार्य होगा क्योंकि यह साइड इफेक्ट हो सकता है या बस एक अलग परिणाम दे सकता है। यह यह भी मानता है कि एंडवाटर हमेशा एक ही मूल्य पर लौटेगा चाहे कितनी भी बार यह कहा जाए और इसे तब कहा जा सकता है जब बिगवाइट झूठे (उपरोक्त धारणा के अनुसार) वापस आए

यह दौड़ की स्थिति के खिलाफ एक गार्ड प्रतीत होता है यदि हम उन बयानों को इनलाइन करते हैं जहां राज्य के बाद एक अलग थ्रेड द्वारा movenext को कॉल किया जाता है = सवालों में 0 यह नीचे की तरह कुछ दिखता है

this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.<>1__state = 0;

//second thread
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.$__doFinallyBodies = true;
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

//other thread
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

यदि उपर्युक्त धारणाएं सही हैं, तो कुछ अनावश्यक कार्य किए गए हैं, जैसे कि सीटर और मान को समान मानकर <1> t __ $ wait1। यदि राज्य को 1 पर रखा गया था, तो अंतिम भाग क्रमशः होगा:

//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

आगे अगर यह 2 राज्य मशीन के लिए सेट किया गया था, तो यह मान लेगा कि यह पहले से ही पहली कार्रवाई का मूल्य प्राप्त कर चुका है जो असत्य होगा और (संभावित) अनसाइनडेड वैरिएबल का उपयोग परिणाम की गणना करने के लिए किया जाएगा


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

@ मैं सहमत हूँ कि यह async मामले में एक दौड़ की स्थिति के साथ एक मुद्दा नहीं होना चाहिए, लेकिन पुनरावृत्ति ब्लॉक में हो सकता है और एक बायीं तरफ हो सकता है
रुस FS

@ टिप्पणी: मुझे लगता है कि मैं अगले CTP या बीटा के आने तक प्रतीक्षा करूंगा, और उस व्यवहार की जांच करूंगा।
जॉन स्कीट

1

क्या यह स्टैक्ड / नेस्टेड एसिंक्स कॉल के साथ कुछ किया जा सकता है?

अर्थात:

async Task m1()
{
    await m2;
}

async Task m2()
{
    await m3();
}

async Task m3()
{
Thread.Sleep(10000);
}

इस स्थिति में Movenext प्रतिनिधि को कई बार बुलाया जाता है?

बस एक पंट वास्तव में?


उस मामले में तीन अलग-अलग उत्पन्न वर्ग होंगे। MoveNext()उनमें से प्रत्येक पर एक बार बुलाया जाएगा।
जॉन स्कीट

0

वास्तविक राज्यों की व्याख्या:

संभव स्थिति:

  • 0 प्रारंभिक (मुझे ऐसा लगता है) या ऑपरेशन के अंत की प्रतीक्षा कर रहा है
  • > 0 बस अगले राज्य को आगे बढ़ाते हुए, MoveNext कहा जाता है
  • -1 समाप्त हुआ

क्या यह संभव है कि यह कार्यान्वयन केवल यह आश्वासन देना चाहता है कि यदि कोई अन्य कॉल-टू-मूवNNN (जहां प्रतीक्षा करते समय) होता है, तो यह शुरुआत से फिर से पूरे राज्य-श्रृंखला का पुनर्मूल्यांकन करेगा, परिणामों का पुनर्मूल्यांकन करने के लिए जो पहले से ही पुराने समय में हो सकता है?


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