async C # 5 CTP: ทำไม“ สถานะ” ภายในถูกตั้งค่าเป็น 0 ในรหัสที่สร้างขึ้นก่อนการโทร EndAwait


195

เมื่อวานนี้ฉันได้พูดคุยเกี่ยวกับคุณลักษณะใหม่ของ "async" C โดยเฉพาะอย่างยิ่งการตรวจสอบสิ่งที่สร้างรหัสดูเหมือนและthe GetAwaiter()/ BeginAwait()/ EndAwait()โทร

เราดูรายละเอียดที่เครื่องสถานะที่สร้างโดยคอมไพเลอร์ C # และมีสองด้านที่เราไม่เข้าใจ:

  • เหตุใดคลาสที่สร้างขึ้นจึงมีDispose()เมธอดและ$__disposingตัวแปรซึ่งไม่เคยปรากฏที่จะใช้ (และคลาสไม่ได้นำไปใช้IDisposable)
  • สาเหตุที่stateตัวแปรภายในถูกตั้งค่าเป็น 0 ก่อนการเรียกใด ๆEndAwait()เมื่อโดยปกติ 0 ปรากฏว่าหมายถึง "นี่คือจุดเริ่มต้น"

ฉันสงสัยว่าจุดแรกสามารถตอบได้โดยทำสิ่งที่น่าสนใจมากขึ้นในวิธีการ async ถึงแม้ว่าหากใครมีข้อมูลเพิ่มเติมฉันก็ยินดีที่จะได้ยิน อย่างไรก็ตามคำถามนี้เกี่ยวกับประเด็นที่สองมากกว่า

นี่คือตัวอย่างโค้ดที่ง่ายมาก:

using System.Threading.Tasks;

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

... และนี่คือรหัสที่ได้รับการสร้างขึ้นสำหรับMoveNext()วิธีการที่ใช้เครื่องรัฐ สิ่งนี้ถูกคัดลอกโดยตรงจาก Reflector - ฉันยังไม่ได้แก้ไขชื่อตัวแปรที่ไม่สามารถบรรยายได้:

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) มันก็จะเริ่มต้นวิธีการซิงค์ได้อย่างมีประสิทธิภาพอีกครั้งซึ่งจะไม่เหมาะสมอย่างเต็มที่เท่าที่ฉันสามารถบอกได้ว่า ... ถ้าMoveNext() ไม่ได้ถูกเรียก

นี่เป็นเพียงผลข้างเคียงของคอมไพเลอร์ที่ใช้รหัสการสร้างบล็อกตัววนซ้ำสำหรับ async ซึ่งอาจมีคำอธิบายที่ชัดเจนกว่านี้หรือไม่

ข้อจำกัดความรับผิดชอบที่สำคัญ

เห็นได้ชัดว่านี่เป็นเพียงคอมไพเลอร์ CTP ฉันคาดหวังอย่างเต็มที่ว่าสิ่งต่าง ๆ จะเปลี่ยนแปลงก่อนที่จะปล่อยรุ่นสุดท้าย - และอาจเป็นไปได้ก่อนถึงรุ่น CTP ถัดไป คำถามนี้ไม่มีทางพยายามที่จะอ้างว่านี่เป็นข้อบกพร่องในคอมไพเลอร์ C # หรืออะไรทำนองนั้น ฉันแค่พยายามคิดออกว่ามีเหตุผลที่ลึกซึ้งสำหรับเรื่องนี้หรือเปล่าที่ฉันพลาด :)


7
คอมไพเลอร์ VB สร้างเครื่องสถานะที่คล้ายกัน (ไม่ทราบว่าเป็นที่คาดหวังหรือไม่ แต่ VB ไม่มีบล็อกตัววนซ้ำก่อนหน้านี้)
Damien_The_Unbeliever

1
@Rune: MoveNextDelegate เป็นเพียงเขตข้อมูลผู้รับมอบสิทธิ์ซึ่งอ้างถึง MoveNext มันเก็บไว้เพื่อหลีกเลี่ยงการสร้างแอคชั่นใหม่เพื่อส่งผ่านไปยังพนักงานเสิร์ฟทุกครั้งที่ฉันเชื่อ
Jon Skeet

5
ฉันคิดว่าคำตอบคือ: นี่คือ CTP ลำดับขั้นสูงสำหรับทีมคือการนำสิ่งนี้ออกไปและการตรวจสอบการออกแบบภาษา และพวกเขาก็ทำอย่างรวดเร็วอย่างน่าอัศจรรย์ คุณควรคาดหวังว่าการใช้งานที่จัดส่ง (ของคอมไพเลอร์ไม่ใช่ MoveNext) จะแตกต่างกันอย่างมีนัยสำคัญ ฉันคิดว่า Eric หรือ Lucian จะกลับมาพร้อมคำตอบตามบรรทัดที่ไม่มีอะไรอยู่ลึก ๆ เพียงพฤติกรรม / ข้อบกพร่องที่ไม่สำคัญในกรณีส่วนใหญ่และไม่มีใครสังเกตเห็น เพราะมันเป็น CTP
Chris Burrows

2
@Stilgar: ฉันเพิ่งตรวจสอบกับ ildasm และมันกำลังทำสิ่งนี้อยู่
Jon Skeet

3
@JonSkeet: สังเกตุว่าไม่มีใครตอบคำตอบได้ 99% ของเราไม่สามารถบอกได้ว่าคำตอบนั้นฟังดูดีหรือเปล่า
the_drow

คำตอบ:


71

โอเคในที่สุดฉันก็มีคำตอบที่แท้จริง ฉันเรียงลำดับจากการทำงานด้วยตัวเอง แต่หลังจาก Lucian Wischik จากส่วน VB ของทีมยืนยันว่ามีเหตุผลที่ดีจริงๆ ขอบคุณมากที่เขา - และโปรดเยี่ยมชมบล็อกของเขาซึ่งหิน

ค่า 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ใด ๆ ที่อาจรอคอยได้ - อาจเป็นภารกิจหรืออย่างอื่นก็ได้ สำหรับวัตถุประสงค์ของการทดสอบของฉันมันเสมอกลับเท็จและโยนข้อยกเว้นในIsCompletedGetResult

นี่คือรหัสที่สร้างขึ้นสำหรับ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ส่งข้อยกเว้น เราจะผ่านบล็อก catch การเพิ่มiและการวนรอบอีกครั้ง (สมมติว่าiยังน้อยกว่า 3) เรายังคงอยู่ในสถานะใดก็ตามที่เราอยู่ก่อนการGetResultโทร ... แต่เมื่อเราเข้าไปในtryบล็อกเราจะต้องพิมพ์ "กำลังทดลอง" และโทรGetAwaiterอีกครั้ง ... และเราจะทำเช่นนั้นหากรัฐไม่ได้เป็น 1 โดยไม่มี การstate = 0มอบหมายมันจะใช้บริกรที่มีอยู่และข้ามConsole.WriteLineสาย

มันเป็นรหัสที่ค่อนข้างคดโกงพอสมควร แต่ก็แสดงให้เห็นถึงสิ่งที่ทีมต้องคิด ฉันดีใจที่ฉันไม่รับผิดชอบในการใช้สิ่งนี้ :)


8
@Shekhar_Pro: ใช่มันเป็น goto คุณควรคาดหวังที่จะเห็นความอุดมสมบูรณ์ของงบข้ามไปในเครื่องรัฐสร้างขึ้นโดยอัตโนมัติ :)
จอนสกีต

12
@Shekhar_Pro: ภายในโค้ดที่เขียนด้วยตนเองมันเป็น - เพราะมันทำให้โค้ดยากที่จะอ่านและปฏิบัติตาม ไม่มีใครอ่านรหัส autogenerated แต่ยกเว้นคนโง่อย่างผมที่แยกมัน :)
จอนสกีต

ดังนั้นจะเกิดอะไรขึ้นเมื่อเรารออีกครั้งหลังจากข้อยกเว้น? เราเริ่มต้นใหม่อีกครั้ง?
กำหนดค่า

1
@configurator: มันเรียก GetAwaiter เกี่ยวกับการรอคอยซึ่งเป็นสิ่งที่ฉันคาดหวังว่าจะทำ
Jon Skeet

gotos ไม่ได้สร้างโค้ดให้อ่านยากกว่าเสมอ ในความเป็นจริงบางครั้งพวกเขาก็มีเหตุผลที่จะใช้ (สิ่งศักดิ์สิทธิ์ที่จะพูดว่าฉันรู้) ตัวอย่างเช่นบางครั้งคุณอาจต้องแบ่งลูปซ้อนกันหลายวง ฉันใช้น้อยกว่าคุณสมบัติของ goto (และการใช้ IMO uglier) คือการทำให้งบเปลี่ยนเป็นน้ำตก ในบันทึกแยกฉันจำวันและอายุเมื่อ gotos เป็นหลักหนุนของภาษาการเขียนโปรแกรมบางส่วนและเนื่องจากการที่ฉันตระหนักดีว่าทำไมการกล่าวถึง goto เพียงทำให้นักพัฒนาสั่น พวกเขาสามารถทำสิ่งที่น่าเกลียดถ้าใช้ไม่ดี
เบ็นเลชส์

5

ถ้ามันถูกเก็บไว้ที่ 1 (กรณีแรก) คุณจะได้รับการเรียกโดยไม่ต้องโทรไปEndAwait BeginAwaitหากเก็บไว้ที่ 2 (ตัวพิมพ์ที่สอง) คุณจะได้ผลลัพธ์เหมือนกันกับพนักงานเสิร์ฟคนอื่น

ฉันเดาว่าการโทร BeginAwait จะส่งกลับค่าเท็จถ้ามันเริ่มต้นแล้ว (เดาจากด้านข้างของฉัน) และเก็บค่าดั้งเดิมไว้ที่ EndAwait หากเป็นกรณีนี้จะทำงานได้อย่างถูกต้องในขณะที่ถ้าคุณตั้งค่าเป็น -1 คุณอาจมีการกำหนดค่าเริ่มต้นthis.<1>t__$await1สำหรับกรณีแรก

อย่างไรก็ตามสิ่งนี้อนุมานว่า BeginAwaiter จะไม่เริ่มการโทรใด ๆ หลังจากการโทรครั้งแรกและจะส่งคืนเท็จในกรณีเหล่านั้น แน่นอนว่าการเริ่มต้นจะไม่สามารถยอมรับได้เนื่องจากอาจมีผลข้างเคียงหรือให้ผลที่แตกต่างกัน นอกจากนี้ยังสมมติว่า EndAwaiter จะส่งคืนค่าเดิมเสมอไม่ว่าจะมีการโทรกี่ครั้งและสามารถเรียกได้เมื่อ BeginAwait ส่งคืนค่าเท็จ (ตามข้อสมมติข้างต้น)

ดูเหมือนว่าจะเป็นเครื่องป้องกันสภาพการแข่งขันหากเราแทรกข้อความที่ 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();

หากข้อสันนิษฐานข้างต้นถูกต้องแสดงว่ามีงานที่ไม่จำเป็นเกิดขึ้นเช่นรับ Sawiater และมอบหมายค่าเดียวกันเป็น <1> t __ $ await1 หากสถานะถูกเก็บไว้ที่ 1 ดังนั้นส่วนสุดท้ายจะเป็น:

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

ต่อไปหากตั้งเป็น 2 เครื่องรัฐจะถือว่ามันได้รับค่าของการกระทำแรกซึ่งจะไม่จริงและตัวแปรที่ไม่ได้กำหนด (อาจเกิดขึ้น) จะถูกใช้เพื่อคำนวณผลลัพธ์


โปรดจำไว้ว่ารัฐไม่ได้ถูกใช้จริงระหว่างการมอบหมายเป็น 0 และการมอบหมายให้เป็นค่าที่มีความหมายมากกว่า หากเป็นการป้องกันสภาพการแข่งขันฉันคาดหวังว่าจะมีค่าอื่น ๆ ที่บ่งบอกว่าเช่น -2 โดยมีการตรวจสอบว่าตอนเริ่มต้นของ MoveNext เพื่อตรวจจับการใช้งานที่ไม่เหมาะสม โปรดจำไว้ว่าอินสแตนซ์เดียวไม่ควรถูกใช้จริงโดยเธรดสองตัวต่อครั้ง - เป็นการให้ภาพลวงตาของการเรียกเมธอดแบบซิงโครนัสเดียวซึ่งจัดการเพื่อ "หยุด" ทุกครั้ง
Jon Skeet

@ จอนฉันเห็นด้วยไม่ควรมีปัญหากับสภาพการแข่งขันในกรณี async แต่อาจจะอยู่ในช่วงการทำซ้ำและอาจจะเหลือ
Rune FS

@ โทนี่: ฉันคิดว่าฉันจะรอจนกว่า CTP หรือเบต้าถัดไปจะออกมาและตรวจสอบพฤติกรรมนั้น
Jon Skeet

1

มันเป็นสิ่งที่จะทำอย่างไรกับการเรียก async แบบซ้อน / ซ้อนกัน?

เช่น:

async Task m1()
{
    await m2;
}

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

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

ผู้รับมอบสิทธิ์ movenext ได้รับการเรียกหลายครั้งในสถานการณ์นี้หรือไม่?

เพียงแค่ถ่อจริงเหรอ?


ในกรณีนั้นจะมีคลาสที่สร้างขึ้นแตกต่างกันสามคลาส MoveNext()จะถูกเรียกหนึ่งครั้งในแต่ละรายการ
Jon Skeet

0

คำอธิบายของสถานะที่แท้จริง:

รัฐที่เป็นไปได้:

  • 0 ถูกเตรียมใช้งาน (ฉันคิดว่างั้น) หรือรอการสิ้นสุด
  • > 0เพิ่งเรียกว่า MoveNext โดยเลือกสถานะถัดไป
  • -1สิ้นสุดวันที่

เป็นไปได้หรือไม่ที่การดำเนินการนี้ต้องการให้มั่นใจว่าหากมีการโทรไปยัง MoveNext อีกครั้งจากที่ใดก็ตามที่เกิดขึ้น (ในระหว่างที่รอ) จะประเมินห่วงโซ่ของรัฐทั้งหมดอีกครั้งตั้งแต่ต้นเพื่อประเมินผลใหม่ซึ่งอาจ


แต่ทำไมมันต้องเริ่มจากจุดเริ่มต้น? นั่นไม่ใช่สิ่งที่คุณต้องการเกิดขึ้นจริง ๆ - คุณต้องการมีข้อยกเว้นเนื่องจากไม่มีสิ่งใดที่ควรจะเรียก MoveNext
Jon Skeet
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.