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$__disposing
variabel, yang sepertinya tidak pernah digunakan (dan kelas tidak mengimplementasikanIDisposable
). - Mengapa
state
variabel internal diatur ke 0 sebelum panggilan apa punEndAwait()
, 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 :)
sumber
Jawaban:
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
await
dalam 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:
Secara konseptual, yang
SimpleAwaitable
bisa ditunggu-tunggu - mungkin tugas, mungkin sesuatu yang lain. Untuk keperluan pengujian saya, selalu menghasilkan false untukIsCompleted
, dan melempar pengecualian dalamGetResult
.Inilah kode yang dihasilkan untuk
MoveNext
:Saya harus pindah
Label_ContinuationPoint
untuk membuatnya menjadi kode yang valid - kalau tidak, tidak dalam lingkupgoto
pernyataan - tetapi itu tidak mempengaruhi jawabannya.Pikirkan tentang apa yang terjadi ketika
GetResult
melempar pengecualiannya. Kami akan pergi melalui blok tangkap, kenaikani
, dan kemudian putaran lagi (dengan asumsii
masih kurang dari 3). Kita masih dalam keadaan apa pun sebelumGetResult
panggilan ... tetapi ketika kita masuk ke dalamtry
blok kita harus mencetak "Coba" dan meneleponGetAwaiter
lagi ... dan kita hanya akan melakukannya jika keadaan tidak 1. Tanpa yangstate = 0
tugas, ia akan menggunakan awaiter ada dan melewatkanConsole.WriteLine
panggilan.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 :)
sumber
jika disimpan pada 1 (kasus pertama) Anda akan mendapatkan panggilan
EndAwait
tanpa panggilan keBeginAwait
. 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__$await1
untuk 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
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:
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
sumber
Mungkinkah ada hubungannya dengan panggilan async ditumpuk / bersarang? ..
yaitu:
Apakah delegasi movenext dipanggil beberapa kali dalam situasi ini?
Benar-benar hanya seorang pesepakbola?
sumber
MoveNext()
akan dipanggil sekali pada masing-masing.Penjelasan kondisi aktual:
negara yang memungkinkan:
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?
sumber