Pola async-await dari .net 4.5 sedang mengubah paradigma. Hampir terlalu bagus untuk menjadi kenyataan.
Saya telah mem-port beberapa kode IO-berat ke async-await karena pemblokiran sudah berlalu.
Beberapa orang membandingkan async-await dengan infestasi zombi dan menurut saya cukup akurat. Kode asinkron menyukai kode asinkron lainnya (Anda memerlukan fungsi asinkron untuk menunggu fungsi asinkron). Jadi, semakin banyak fungsi menjadi asinkron dan ini terus berkembang di basis kode Anda.
Mengubah fungsi menjadi async adalah pekerjaan yang berulang dan tidak imajinatif. Lemparasync
kata kunci dalam deklarasi, bungkus nilai yang dikembalikan dengan Task<>
dan Anda sudah cukup banyak selesai. Agak meresahkan betapa mudahnya seluruh proses, dan segera skrip pengganti teks akan mengotomatiskan sebagian besar "porting" untuk saya.
Dan sekarang pertanyaannya .. Jika semua kode saya perlahan berubah menjadi asinkron, mengapa tidak menjadikan semuanya asinkron secara default?
Alasan jelas yang saya asumsikan adalah kinerja. Async-await memiliki overhead dan kode yang tidak perlu async, sebaiknya tidak. Tetapi jika kinerja adalah satu-satunya masalah, pasti beberapa pengoptimalan yang cerdas dapat menghilangkan overhead secara otomatis saat tidak diperlukan. Saya telah membaca tentang pengoptimalan "jalur cepat" , dan menurut saya pengoptimalan itu sendirilah yang menangani sebagian besar.
Mungkin ini sebanding dengan pergeseran paradigma yang dilakukan para pemulung. Di masa awal GC, membebaskan memori Anda sendiri pasti lebih efisien. Tetapi massa masih memilih pengumpulan otomatis untuk mendukung kode yang lebih aman, lebih sederhana yang mungkin kurang efisien (dan bahkan itu bisa dibilang tidak benar lagi). Mungkin ini yang harus terjadi di sini? Mengapa semua fungsi tidak boleh asinkron?
sumber
async
(untuk memenuhi beberapa kontrak), ini mungkin ide yang buruk. Anda mendapatkan kerugian dari async (peningkatan biaya panggilan metode; kebutuhan untuk menggunakanawait
kode panggilan), tetapi tidak ada keuntungannya.Jawaban:
Pertama, terima kasih atas kata-kata baik Anda. Ini memang fitur yang luar biasa dan saya senang menjadi bagian kecil darinya.
Nah, Anda melebih-lebihkan; semua kode Anda tidak berubah menjadi asinkron. Saat Anda menambahkan dua bilangan bulat "biasa", Anda tidak menunggu hasilnya. Ketika Anda menambahkan dua bilangan bulat masa depan bersama-sama untuk mendapatkan bilangan bulat masa depan ketiga - karena itulah
Task<int>
, itu adalah bilangan bulat yang akan Anda dapatkan aksesnya di masa depan - tentu saja Anda kemungkinan besar akan menunggu hasilnya.Alasan utama untuk tidak menjadikan semuanya asinkron adalah karena tujuan async / await adalah untuk mempermudah penulisan kode di dunia dengan banyak operasi latensi tinggi . Sebagian besar operasi Anda bukanlah latensi tinggi, jadi tidak masuk akal untuk mengambil hasil kinerja yang mengurangi latensi itu. Sebaliknya, beberapa kunci dari operasi Anda adalah latensi tinggi, dan operasi tersebut menyebabkan infestasi zombie dari async di seluruh kode.
Secara teori, teori dan praktek serupa. Dalam praktiknya, mereka tidak pernah seperti itu.
Izinkan saya memberi Anda tiga poin terhadap transformasi semacam ini yang diikuti dengan pengoptimalan.
Poin pertama lagi adalah: async di C # / VB / F # pada dasarnya adalah bentuk kelanjutan yang terbatas . Sejumlah besar penelitian dalam komunitas bahasa fungsional telah mencari cara untuk mengidentifikasi cara mengoptimalkan kode yang memanfaatkan gaya penerusan lanjutan. Tim kompilator mungkin harus memecahkan masalah yang sangat mirip di dunia di mana "async" adalah defaultnya dan metode non-async harus diidentifikasi dan dide-async-ified. Tim C # tidak terlalu tertarik untuk menangani masalah penelitian terbuka, jadi itulah poin besar yang harus dibantah di sana.
Hal kedua yang bertentangan adalah bahwa C # tidak memiliki tingkat "transparansi referensial" yang membuat pengoptimalan semacam ini lebih mudah diatur. Yang saya maksud dengan "transparansi referensial" adalah properti yang nilai ekspresi tidak bergantung saat dievaluasi . Ekspresi seperti
2 + 2
secara referensial transparan; Anda dapat melakukan evaluasi pada waktu kompilasi jika diinginkan, atau menundanya hingga waktu proses dan mendapatkan jawaban yang sama. Tetapi ekspresi sepertix+y
tidak dapat dipindahkan seiring waktu karena x dan y mungkin berubah seiring waktu .Asinkron membuat lebih sulit untuk bernalar tentang kapan efek samping akan terjadi. Sebelum asinkron, jika Anda mengatakan:
dan
M()
ituvoid M() { Q(); R(); }
, danN()
ituvoid N() { S(); T(); }
, danR
danS
efek samping hasil, maka Anda tahu bahwa efek samping R terjadi sebelum efek samping S. Tetapi jika Andaasync void M() { await Q(); R(); }
tiba-tiba itu keluar jendela. Anda tidak punya jaminan apakahR()
akan terjadi sebelum atau sesudahS()
(kecuali tentu sajaM()
ditunggu; tapi tentu sajaTask
tidak perlu ditunggu sampai setelahnyaN()
.)Sekarang bayangkan bahwa properti ini tidak lagi mengetahui efek samping urutan apa yang terjadi berlaku untuk setiap bagian kode dalam program Anda kecuali yang berhasil di-de-async-ify oleh pengoptimal. Pada dasarnya Anda tidak memiliki petunjuk lagi ekspresi mana yang akan dievaluasi dalam urutan apa, yang berarti bahwa semua ekspresi harus transparan secara referensial, yang sulit dalam bahasa seperti C #.
Hal ketiga yang menentang adalah bahwa Anda kemudian harus bertanya "mengapa asinkron begitu istimewa?" Jika Anda akan berargumen bahwa setiap operasi harus benar-benar menjadi,
Task<T>
maka Anda harus dapat menjawab pertanyaan "mengapa tidakLazy<T>
?" atau "kenapa tidakNullable<T>
?" atau "kenapa tidakIEnumerable<T>
?" Karena kita bisa melakukannya dengan mudah. Mengapa tidak setiap operasi diangkat menjadi nullable ? Atau setiap operasi dihitung dengan malas dan hasilnya disimpan dalam cache untuk nanti , atau hasil dari setiap operasi adalah urutan nilai, bukan hanya satu nilai . Anda kemudian harus mencoba untuk mengoptimalkan situasi di mana Anda tahu "oh, ini tidak boleh nol, jadi saya dapat menghasilkan kode yang lebih baik", dan seterusnya.Intinya: tidak jelas bagi saya bahwa
Task<T>
sebenarnya itu istimewa untuk menjamin pekerjaan sebanyak ini.Jika hal-hal semacam ini menarik minat Anda, saya sarankan Anda menyelidiki bahasa fungsional seperti Haskell, yang memiliki transparansi referensial yang lebih kuat dan mengizinkan semua jenis evaluasi out-of-order dan melakukan caching otomatis. Haskell juga memiliki dukungan yang jauh lebih kuat dalam sistem tipenya untuk jenis "pengangkatan monad" yang telah saya singgung.
sumber
async/await
karena sesuatu yang tersembunyi di bawah lapisan abstraksi mungkin tidak sinkron.Performa adalah salah satu alasannya, seperti yang Anda sebutkan. Perhatikan bahwa opsi "jalur cepat" yang Anda tautkan memang meningkatkan kinerja dalam kasus Tugas yang sudah selesai, tetapi masih memerlukan lebih banyak instruksi dan overhead dibandingkan dengan satu panggilan metode. Dengan demikian, bahkan dengan "jalur cepat" di tempat, Anda menambahkan banyak kerumitan dan overhead dengan setiap panggilan metode asinkron.
Kompatibilitas mundur, serta kompatibilitas dengan bahasa lain (termasuk skenario interop), juga akan menjadi masalah.
Yang lainnya adalah masalah kompleksitas dan niat. Operasi asinkron menambah kompleksitas - dalam banyak kasus, fitur bahasa menyembunyikannya, tetapi ada banyak kasus di mana metode pembuatan
async
benar-benar menambah kompleksitas pada penggunaannya. Ini terutama benar jika Anda tidak memiliki konteks sinkronisasi, karena metode asinkron kemudian dapat dengan mudah menyebabkan masalah threading yang tidak terduga.Selain itu, ada banyak rutinitas yang tidak, menurut sifatnya, tidak sinkron. Itu lebih masuk akal sebagai operasi sinkron. Memaksa
Math.Sqrt
untuk menjadiTask<double> Math.SqrtAsync
akan konyol, misalnya, karena tidak ada alasan sama sekali untuk itu menjadi asynchronous. Alih-alihasync
mendorong melalui aplikasi Anda, Anda akan berakhir denganawait
menyebar ke mana-mana .Ini juga akan mematahkan paradigma saat ini sepenuhnya, serta menyebabkan masalah dengan properti (yang secara efektif hanya merupakan pasangan metode .. apakah mereka akan menjadi asinkron juga?), Dan memiliki dampak lain di seluruh desain kerangka kerja dan bahasa.
Jika Anda melakukan banyak pekerjaan terikat IO, Anda akan cenderung menemukan bahwa menggunakan
async
pervasively adalah tambahan yang bagus, banyak dari rutinitas Anda jugaasync
. Namun, ketika Anda mulai melakukan pekerjaan terikat CPU, secara umum, membuat sesuatuasync
sebenarnya tidak baik - ini menyembunyikan fakta bahwa Anda menggunakan siklus CPU di bawah API yang tampak asinkron, tetapi sebenarnya tidak selalu benar-benar asinkron.sumber
await FooAsync()
lebih sederhana dariFoo()
? Dan alih-alih efek domino kecil beberapa waktu, Anda memiliki efek domino besar sepanjang waktu dan Anda menyebutnya peningkatan?Selain kinerja - asinkron dapat menimbulkan biaya produktivitas. Pada klien (WinForms, WPF, Windows Phone) ini adalah keuntungan bagi produktivitas. Tetapi di server, atau dalam skenario non-UI lainnya, Anda membayar produktivitas. Anda tentu tidak ingin menggunakan asinkron secara default di sana. Gunakan saat Anda membutuhkan keunggulan skalabilitas.
Gunakan saat di sweet spot. Dalam kasus lain, jangan.
sumber
Saya yakin ada alasan bagus untuk membuat semua metode asinkron jika tidak diperlukan - ekstensibilitas. Metode pembuatan selektif asinkron hanya berfungsi jika kode Anda tidak pernah berkembang dan Anda tahu bahwa metode A () selalu terikat dengan CPU (Anda tetap menyinkronkannya) dan metode B () selalu terikat I / O (Anda menandainya sebagai asinkron).
Tapi bagaimana jika ada perubahan? Ya, A () sedang melakukan penghitungan tetapi di beberapa titik di masa mendatang Anda harus menambahkan pencatatan di sana, atau pelaporan, atau panggilan balik yang ditentukan pengguna dengan implementasi yang tidak dapat diprediksi, atau algoritme telah diperpanjang dan sekarang tidak hanya mencakup penghitungan CPU tetapi juga beberapa I / O? Anda harus mengonversi metode menjadi asinkron tetapi ini akan merusak API dan semua pemanggil di atas tumpukan juga perlu diperbarui (dan mereka bahkan dapat menjadi aplikasi berbeda dari vendor yang berbeda). Atau Anda perlu menambahkan versi asinkron bersama dengan versi sinkronisasi tetapi ini tidak membuat banyak perbedaan - menggunakan versi sinkronisasi akan memblokir dan karenanya hampir tidak dapat diterima.
Akan sangat bagus jika memungkinkan untuk membuat metode sinkronisasi yang ada menjadi asinkron tanpa mengubah API. Tetapi pada kenyataannya kami tidak memiliki opsi seperti itu, saya yakin, dan menggunakan versi async meskipun saat ini tidak diperlukan adalah satu-satunya cara untuk menjamin Anda tidak akan pernah mengalami masalah kompatibilitas di masa mendatang.
sumber