Pertama, terima kasih atas kata-kata baik Anda. Ini memang fitur yang luar biasa dan saya senang menjadi bagian kecil darinya.
Jika semua kode saya perlahan berubah menjadi asinkron, mengapa tidak menjadikan semuanya asinkron secara default?
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.
jika kinerja adalah satu-satunya masalah, pasti beberapa pengoptimalan yang cerdas dapat menghilangkan overhead secara otomatis saat tidak diperlukan.
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 seperti x+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:
M();
N();
dan M()
itu void M() { Q(); R(); }
, dan N()
itu void N() { S(); T(); }
, dan R
dan S
efek samping hasil, maka Anda tahu bahwa efek samping R terjadi sebelum efek samping S. Tetapi jika Anda async void M() { await Q(); R(); }
tiba-tiba itu keluar jendela. Anda tidak punya jaminan apakah R()
akan terjadi sebelum atau sesudah S()
(kecuali tentu saja M()
ditunggu; tapi tentu saja Task
tidak perlu ditunggu sampai setelahnya N()
.)
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 tidak Lazy<T>
?" atau "kenapa tidak Nullable<T>
?" atau "kenapa tidak IEnumerable<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.