C # 5 async CTP: mengapa internal "state" diatur ke 0 dalam kode yang dihasilkan sebelum EndAwait panggilan?


195

Kemarin saya memberikan ceramah tentang fitur C # "async" yang baru, khususnya menyelidiki seperti apa kode yang dihasilkan, dan the GetAwaiter()/ BeginAwait()/ EndAwait()panggilan.

Kami melihat secara rinci mesin negara yang dihasilkan oleh kompiler C #, dan ada dua aspek yang tidak dapat kami pahami:

  • Mengapa kelas yang dihasilkan berisi Dispose()metode dan $__disposingvariabel, yang sepertinya tidak pernah digunakan (dan kelas tidak mengimplementasikan IDisposable).
  • Mengapa statevariabel internal diatur ke 0 sebelum panggilan apa pun EndAwait(), ketika 0 biasanya muncul berarti "ini adalah titik masuk awal".

Saya menduga poin pertama dapat dijawab dengan melakukan sesuatu yang lebih menarik dalam metode async, walaupun jika ada yang punya informasi lebih lanjut saya akan senang mendengarnya. Pertanyaan ini lebih banyak tentang poin kedua.

Berikut ini contoh kode sampel yang sangat sederhana:

using System.Threading.Tasks;

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

... dan inilah kode yang dihasilkan untuk MoveNext()metode yang mengimplementasikan mesin negara. Ini disalin langsung dari Reflektor - Saya belum memperbaiki nama variabel yang tidak dapat diucapkan:

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);
    }
}

Itu panjang, tetapi kalimat penting untuk pertanyaan ini adalah:

// 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();

Dalam kedua kasus keadaan diubah lagi setelah itu sebelum berikutnya jelas diamati ... jadi mengapa set ke 0 sama sekali? Jika MoveNext()dipanggil lagi pada titik ini (baik secara langsung atau melalui Dispose) itu akan secara efektif memulai metode async lagi, yang akan sepenuhnya tidak sesuai sejauh yang saya tahu ... jika dan MoveNext() tidak dipanggil, perubahan status tidak relevan.

Apakah ini hanya efek samping dari kompilator menggunakan kembali kode pembangkitan iterator blok untuk async, di mana ia mungkin memiliki penjelasan yang lebih jelas?

Penafian penting

Jelas ini hanyalah kompiler CTP. Saya sepenuhnya berharap hal-hal berubah sebelum rilis final - dan bahkan mungkin sebelum rilis CTP berikutnya. Pertanyaan ini sama sekali tidak mencoba untuk mengklaim ini adalah cacat dalam kompiler C # atau sesuatu seperti itu. Saya hanya mencoba mencari tahu apakah ada alasan halus untuk ini yang saya lewatkan :)


7
Kompiler VB menghasilkan mesin negara yang serupa (tidak tahu apakah itu diharapkan atau tidak, tetapi VB tidak memiliki blok iterator sebelumnya)
Damien_The_Unbeliever

1
@ Run: MoveNextDelegate hanyalah bidang delegasi yang merujuk ke MoveNext. Tembolok itu di-cache untuk menghindari membuat Action baru untuk masuk ke penunggu setiap kali, saya percaya.
Jon Skeet

5
Saya pikir jawabannya adalah: ini adalah CTP. Urutan tinggi untuk tim adalah mendapatkan ini di luar sana dan desain bahasa divalidasi. Dan mereka melakukannya dengan sangat cepat. Anda harus mengharapkan implementasi yang dikirimkan (dari kompiler, bukan MoveNext) berbeda secara signifikan. Saya pikir Eric atau Lucian kembali dengan jawaban di sepanjang garis bahwa tidak ada yang dalam di sini, hanya perilaku / bug yang tidak masalah dalam banyak kasus dan tidak ada yang memperhatikan. Karena ini adalah CTP.
Chris Burrows

2
@ Stilgar: Saya baru saja memeriksa ildasm, dan itu benar-benar melakukan ini.
Jon Skeet

3
@JonSkeet: Perhatikan bagaimana tidak ada yang mengubah jawaban. 99% dari kita tidak dapat benar-benar mengetahui apakah jawabannya terdengar benar.
the_drow

Jawaban:


71

Oke, saya akhirnya punya jawaban nyata. Saya semacam mengerjakannya sendiri, tetapi hanya setelah Lucian Wischik dari bagian VB tim mengkonfirmasi bahwa memang ada alasan bagus untuk itu. Banyak terima kasih kepadanya - dan silakan kunjungi blog-nya , yang mengejutkan.

Nilai 0 di sini hanya khusus karena ini bukan keadaan yang valid yang mungkin Anda berada tepat sebelum awaitdalam kasus normal. Secara khusus, ini bukan kondisi yang mungkin berakhir pada pengujian mesin di negara lain. Saya percaya bahwa menggunakan nilai non-positif akan bekerja dengan baik: -1 tidak digunakan untuk ini karena secara logis salah, karena -1 biasanya berarti "selesai". Saya bisa berpendapat bahwa kami memberikan makna ekstra untuk menyatakan 0 saat ini, tetapi pada akhirnya itu tidak terlalu penting. Inti dari pertanyaan ini adalah mencari tahu mengapa negara diatur sama sekali.

Nilai ini relevan jika menunggu berakhir dengan pengecualian yang tertangkap. Kita dapat kembali lagi ke pernyataan menunggu yang sama, tetapi kita tidak boleh berada dalam status yang berarti "Saya baru saja akan kembali dari menunggu itu" karena jika tidak semua jenis kode akan dilewati. Ini paling sederhana untuk menunjukkan ini dengan sebuah contoh. Perhatikan bahwa saya sekarang menggunakan CTP kedua, jadi kode yang dihasilkan sedikit berbeda dengan yang ada di pertanyaan.

Inilah metode 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;
}

Secara konseptual, yang SimpleAwaitablebisa ditunggu-tunggu - mungkin tugas, mungkin sesuatu yang lain. Untuk keperluan pengujian saya, selalu menghasilkan false untuk IsCompleted, dan melempar pengecualian dalam GetResult.

Inilah kode yang dihasilkan untuk 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);
}

Saya harus pindah Label_ContinuationPointuntuk membuatnya menjadi kode yang valid - kalau tidak, tidak dalam lingkup gotopernyataan - tetapi itu tidak mempengaruhi jawabannya.

Pikirkan tentang apa yang terjadi ketika GetResultmelempar pengecualiannya. Kami akan pergi melalui blok tangkap, kenaikan i, dan kemudian putaran lagi (dengan asumsi imasih kurang dari 3). Kita masih dalam keadaan apa pun sebelum GetResultpanggilan ... tetapi ketika kita masuk ke dalam tryblok kita harus mencetak "Coba" dan menelepon GetAwaiterlagi ... dan kita hanya akan melakukannya jika keadaan tidak 1. Tanpa yang state = 0tugas, ia akan menggunakan awaiter ada dan melewatkan Console.WriteLinepanggilan.

Ini adalah kode yang agak berliku untuk dikerjakan, tetapi itu hanya menunjukkan jenis hal yang harus dipikirkan oleh tim. Saya senang saya tidak bertanggung jawab untuk mengimplementasikan ini :)


8
@Shekhar_Pro: Ya, ini kebohongan. Anda harus mengharapkan untuk melihat banyak goto pernyataan dalam mesin negara yang dihasilkan otomatis :)
Jon Skeet

12
@Shekhar_Pro: Dalam kode yang ditulis secara manual, itu - karena membuat kode sulit dibaca dan diikuti. Tidak ada yang membaca kode autogenerated, kecuali orang bodoh seperti saya yang mendekompilasi :)
Jon Skeet

Jadi apa yang terjadi ketika kita menunggu lagi setelah pengecualian? Kita mulai dari awal lagi?
konfigurator

1
@configurator: Ini memanggil GetAwaiter di tunggu, itulah yang saya harapkan untuk dilakukan.
Jon Skeet

goto tidak selalu membuat kode lebih sulit dibaca. Bahkan, kadang-kadang mereka bahkan masuk akal untuk menggunakan (penghinaan untuk mengatakan, saya tahu). Misalnya, kadang-kadang Anda mungkin perlu memutus beberapa loop bersarang. Saya lebih jarang menggunakan fitur goto (dan penggunaan IMO yang lebih buruk) adalah untuk menyebabkan pernyataan switch mengalir. Pada catatan terpisah, saya ingat hari dan usia ketika goto adalah fondasi utama dari beberapa bahasa pemrograman dan karena itu saya sepenuhnya menyadari mengapa penyebutan goto membuat pengembang bergidik. Mereka dapat membuat hal-hal buruk jika digunakan dengan buruk.
Ben Lesh

5

jika disimpan pada 1 (kasus pertama) Anda akan mendapatkan panggilan EndAwaittanpa panggilan ke BeginAwait. Jika disimpan di 2 (kasus kedua) Anda akan mendapatkan hasil yang sama hanya pada penunggu lainnya.

Saya menduga bahwa memanggil BeginAwait mengembalikan false jika sudah dimulai (tebakan dari pihak saya) dan menjaga nilai asli untuk kembali di EndAwait. Jika itu kasusnya akan bekerja dengan benar sedangkan jika Anda mengaturnya ke -1 Anda mungkin memiliki inisialisasi this.<1>t__$await1untuk kasus pertama.

Namun ini mengasumsikan bahwa BeginAwaiter tidak akan benar-benar memulai tindakan pada panggilan apa pun setelah panggilan pertama dan akan mengembalikan false dalam kasus-kasus tersebut. Memulai tentu saja tidak dapat diterima karena dapat memiliki efek samping atau hanya memberikan hasil yang berbeda. Ini juga mengasumsikan bahwa EndAwaiter akan selalu mengembalikan nilai yang sama tidak peduli berapa kali ia dipanggil dan itu dapat dipanggil ketika BeginAwait mengembalikan false (sesuai dengan asumsi di atas)

Tampaknya akan menjadi penjaga terhadap kondisi ras Jika kita sebaris pernyataan di mana movenext disebut oleh utas berbeda setelah negara = 0 dalam pertanyaan itu kita akan terlihat seperti di bawah ini

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();

Jika asumsi di atas benar, ada beberapa pekerjaan yang tidak perlu dilakukan seperti mendapatkan sawiater dan menugaskan nilai yang sama ke <1> t __ $ await1. Jika negara disimpan pada 1 maka bagian terakhir akan menjadi:

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

lebih lanjut jika diatur ke 2 mesin negara akan menganggap sudah mendapatkan nilai dari tindakan pertama yang tidak benar dan (berpotensi) variabel yang tidak ditugaskan akan digunakan untuk menghitung hasilnya


Ingatlah bahwa keadaan sebenarnya tidak digunakan antara penugasan ke 0 dan penugasan ke nilai yang lebih bermakna. Jika itu dimaksudkan untuk menjaga dari kondisi balapan, saya berharap beberapa nilai lain menunjukkan bahwa, misalnya -2, dengan tanda centang di awal MoveNext untuk mendeteksi penggunaan yang tidak pantas. Ingatlah bahwa instance tunggal seharusnya tidak pernah benar-benar digunakan oleh dua utas sekaligus - itu dimaksudkan untuk memberikan ilusi panggilan metode sinkron tunggal yang berhasil "berhenti" sesekali.
Jon Skeet

@Jon Saya setuju itu seharusnya tidak menjadi masalah dengan kondisi balapan dalam async case tetapi bisa di blok iterasi dan bisa menjadi sisa
Rune FS

@Tony: Saya pikir saya akan menunggu sampai CTP atau beta berikutnya keluar, dan periksa perilaku itu.
Jon Skeet

1

Mungkinkah ada hubungannya dengan panggilan async ditumpuk / bersarang? ..

yaitu:

async Task m1()
{
    await m2;
}

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

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

Apakah delegasi movenext dipanggil beberapa kali dalam situasi ini?

Benar-benar hanya seorang pesepakbola?


Akan ada tiga kelas yang dihasilkan berbeda dalam kasus itu. MoveNext()akan dipanggil sekali pada masing-masing.
Jon Skeet

0

Penjelasan kondisi aktual:

negara yang memungkinkan:

  • 0 Diinisialisasi (saya pikir begitu) atau menunggu akhir operasi
  • > 0 baru saja dipanggil MoveNext, memilih status berikutnya
  • -1 berakhir

Apakah mungkin bahwa implementasi ini hanya ingin memastikan bahwa jika Panggilan lain ke MoveNext dari mana pun terjadi (sambil menunggu) itu akan mengevaluasi kembali seluruh rantai negara lagi dari awal, untuk mengevaluasi kembali hasil yang mungkin dalam waktu yang berarti sudah usang?


Tetapi mengapa itu ingin dimulai dari awal? Itu hampir pasti tidak apa Anda benar-benar ingin terjadi - Anda ingin pengecualian dilemparkan, karena tidak ada yang lain harus menelepon MoveNext.
Jon Skeet
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.