Mengapa seseorang menggunakan Tugas <T> di atas ValueTask <T> di C #?

168

Pada C # 7.0, metode async dapat mengembalikan ValueTask <T>. Penjelasan mengatakan bahwa itu harus digunakan ketika kita memiliki hasil cache atau mensimulasikan async melalui kode sinkron. Namun saya masih tidak mengerti apa masalah dengan menggunakan ValueTask selalu atau bahkan mengapa async / menunggu tidak dibangun dengan tipe nilai sejak awal. Kapan ValueTask gagal melakukan pekerjaan itu?

Stilgar
sumber
7
Saya menduga itu ada hubungannya dengan manfaat ValueTask<T>(dalam hal alokasi) tidak terwujud untuk operasi yang sebenarnya tidak sinkron (karena dalam kasus ValueTask<T>itu masih perlu alokasi tumpukan). Ada juga masalah Task<T>memiliki banyak dukungan lain di perpustakaan.
Jon Skeet
3
@JonSkeet pustaka yang ada adalah masalah tapi ini menimbulkan pertanyaan haruskah Tugas telah ValueTask dari awal? Manfaatnya mungkin tidak ada ketika menggunakannya untuk hal-hal async yang sebenarnya tetapi apakah itu berbahaya?
Stilgar
8
Lihat github.com/dotnet/corefx/issues/4708#issuecomment-160658188 untuk lebih banyak kebijaksanaan daripada yang bisa saya sampaikan :)
Jon Skeet
3
@ JoelMueller plotnya mengental :)
Stilgar

Jawaban:

245

Dari dokumen API (penekanan ditambahkan):

Metode dapat mengembalikan instance dari tipe nilai ini ketika kemungkinan bahwa hasil operasi mereka akan tersedia secara serempak dan ketika metode tersebut diharapkan akan sering digunakan sehingga biaya mengalokasikan yang baru Task<TResult>untuk setiap panggilan akan menjadi penghalang.

Ada tradeoff untuk menggunakan ValueTask<TResult>bukan Task<TResult>. Misalnya, sementara a ValueTask<TResult>dapat membantu menghindari alokasi dalam kasus di mana hasil yang sukses tersedia secara serempak, itu juga berisi dua bidang sedangkan a Task<TResult>sebagai jenis referensi adalah bidang tunggal. Ini berarti bahwa pemanggilan metode berakhir dengan mengembalikan dua bidang nilai data, bukan satu, yang lebih banyak data untuk disalin. Ini juga berarti bahwa jika suatu metode yang mengembalikan salah satunya ditunggu dalam suatu asyncmetode, mesin negara untuk asyncmetode itu akan lebih besar karena perlu menyimpan struct yang dua bidang bukannya satu referensi.

Lebih lanjut, untuk penggunaan selain mengkonsumsi hasil operasi asinkron melalui await, ValueTask<TResult>dapat mengarah pada model pemrograman yang lebih berbelit-belit, yang pada gilirannya dapat benar-benar mengarah pada alokasi yang lebih banyak. Misalnya, pertimbangkan metode yang bisa mengembalikan a Task<TResult>dengan tugas yang di-cache sebagai hasil umum atau a ValueTask<TResult>. Jika konsumen hasil ingin menggunakannya sebagai Task<TResult>, seperti untuk digunakan dengan dalam metode seperti Task.WhenAlldan Task.WhenAny, ValueTask<TResult>pertama-tama perlu dikonversi menjadi Task<TResult>penggunaan AsTask, yang mengarah ke alokasi yang akan dihindari jika cache Task<TResult>telah digunakan di tempat pertama.

Dengan demikian, pilihan default untuk setiap metode asinkron adalah mengembalikan Taskatau Task<TResult>. Hanya jika analisis kinerja membuktikannya layak, sebaiknya ValueTask<TResult>digunakan Task<TResult>.

Stephen Cleary
sumber
7
@MattThomas: Ini menghemat Taskalokasi tunggal (yang kecil dan murah hari ini), tetapi dengan biaya membuat alokasi pemanggil yang ada lebih besar dan menggandakan ukuran nilai kembali (berdampak pada alokasi register). Meskipun ini merupakan pilihan yang jelas untuk skenario baca buffered, menerapkannya secara default ke semua antarmuka bukanlah sesuatu yang saya sarankan.
Stephen Cleary
1
Benar, salah satu Taskatau ValueTaskdapat digunakan sebagai tipe pengembalian sinkron (dengan Task.FromResult). Tetapi masih ada nilai (heh) di ValueTaskjika Anda memiliki sesuatu yang Anda harapkan akan sinkron. ReadByteAsyncmenjadi contoh klasik. Saya percaya ValueTask diciptakan terutama untuk "saluran" baru (byte level rendah stream), mungkin juga digunakan dalam inti ASP.NET di mana kinerja sangat penting.
Stephen Cleary
1
Oh saya tahu lol itu, hanya ingin tahu apakah Anda memiliki sesuatu untuk ditambahkan ke komentar spesifik;)
julealgon
2
Apakah PR ini mengalihkan saldo ke lebih suka ValueTask? (ref: blog.marcgravell.com/2019/08/... )
stuartd
2
@stuartd: Untuk saat ini, saya masih merekomendasikan penggunaan Task<T>sebagai default. Ini hanya karena sebagian besar pengembang tidak terbiasa dengan pembatasan sekitar ValueTask<T>(khususnya, aturan "hanya konsumsi sekali" dan aturan "tidak ada pemblokiran"). Yang mengatakan, jika semua pengembang di tim Anda baik ValueTask<T>, maka saya akan merekomendasikan pedoman tingkat tim lebih suka ValueTask<T>.
Stephen Cleary
104

Namun saya masih tidak mengerti apa masalah dengan menggunakan ValueTask selalu

Jenis Struct tidak gratis. Menyalin struct yang lebih besar dari ukuran referensi bisa lebih lambat daripada menyalin referensi. Menyimpan struct yang lebih besar dari referensi membutuhkan lebih banyak memori daripada menyimpan referensi. Structs yang lebih besar dari 64 bit mungkin tidak terdaftar ketika referensi bisa didaftarkan. Manfaat dari tekanan pengumpulan yang lebih rendah mungkin tidak melebihi biaya.

Masalah kinerja harus didekati dengan disiplin teknik. Buat tujuan, ukur kemajuan Anda terhadap sasaran, dan kemudian putuskan bagaimana memodifikasi program jika sasaran tidak terpenuhi, mengukur sepanjang jalan untuk memastikan bahwa perubahan Anda benar-benar perbaikan.

mengapa async / wait tidak dibangun dengan tipe nilai dari awal.

awaittelah ditambahkan ke C # lama setelah Task<T>tipe sudah ada. Akan agak aneh untuk menciptakan tipe baru ketika sudah ada. Dan awaitmelewati banyak iterasi desain sebelum memutuskan yang dikirimkan pada tahun 2012. Yang sempurna adalah musuh dari yang baik; lebih baik untuk mengirimkan solusi yang berfungsi baik dengan infrastruktur yang ada dan kemudian jika ada permintaan pengguna, berikan perbaikan nanti.

Saya perhatikan juga bahwa fitur baru yang memungkinkan tipe yang dipasok pengguna menjadi output dari metode yang dibuat oleh kompiler menambah risiko dan beban pengujian. Ketika satu-satunya hal yang dapat Anda kembalikan adalah batal atau tugas, tim pengujian tidak harus mempertimbangkan skenario apa pun di mana beberapa tipe yang benar-benar gila dikembalikan. Menguji kompiler berarti mencari tahu tidak hanya program apa yang orang mungkin tulis, tetapi program apa yang mungkin ditulis, karena kami ingin kompiler mengkompilasi semua program legal, bukan hanya semua program yang masuk akal. Itu mahal.

Dapatkah seseorang menjelaskan kapan ValueTask akan gagal melakukan pekerjaan itu?

Tujuan dari hal ini adalah peningkatan kinerja. Itu tidak melakukan pekerjaan jika tidak meningkatkan kinerja secara terukur dan signifikan . Tidak ada jaminan bahwa itu akan terjadi.

Eric Lippert
sumber
23

ValueTask<T>bukan bagian dari Task<T>, itu superset .

ValueTask<T>adalah penyatuan terdiskriminasi dari T dan a Task<T>, membuatnya bebas alokasi untuk ReadAsync<T>mengembalikan nilai T yang tersedia secara sinkron (berbeda dengan menggunakan Task.FromResult<T>, yang perlu mengalokasikan Task<T>instance). ValueTask<T>dapat ditunggu, sehingga sebagian besar konsumsi instance tidak dapat dibedakan dengan Task<T>.

ValueTask, sebagai sebuah struct, memungkinkan penulisan metode async yang tidak mengalokasikan memori ketika mereka berjalan secara sinkron tanpa mengurangi konsistensi API. Bayangkan memiliki antarmuka dengan metode pengembalian tugas. Setiap kelas yang mengimplementasikan antarmuka ini harus mengembalikan Tugas bahkan jika mereka mengeksekusi secara bersamaan (mudah-mudahan menggunakan Task.FromResult). Anda tentu saja dapat memiliki 2 metode yang berbeda pada antarmuka, yang sinkron dan async tetapi ini membutuhkan 2 implementasi yang berbeda untuk menghindari "sinkronisasi lebih dari async" dan "async atas sinkronisasi".

Jadi itu memungkinkan Anda menulis satu metode yang asinkron atau sinkron, daripada menulis satu metode yang identik untuk masing-masing. Anda bisa menggunakannya di mana saja Anda gunakan Task<T>tetapi seringkali tidak akan menambahkan apa pun.

Yah, itu menambahkan satu hal: Ini menambahkan janji tersirat kepada pemanggil bahwa metode ini benar-benar menggunakan fungsionalitas tambahan yang ValueTask<T>menyediakan. Saya pribadi lebih suka memilih parameter dan mengembalikan tipe yang memberi tahu penelepon sebanyak mungkin. Jangan kembali IList<T>jika enumerasi tidak dapat memberikan hitungan; jangan kembali IEnumerable<T>jika itu bisa. Konsumen Anda seharusnya tidak perlu mencari dokumentasi apa pun untuk mengetahui metode mana yang dapat disebut secara serempak dan mana yang tidak.

Saya tidak melihat perubahan desain di masa depan sebagai argumen yang meyakinkan di sana. Justru sebaliknya: Jika suatu metode mengubah semantiknya, itu harus memecah bangunan sampai semua panggilan ke itu diperbarui sesuai. Jika itu dianggap tidak diinginkan (dan percayalah, saya bersimpati pada keinginan untuk tidak merusak build), pertimbangkan versi antarmuka.

Ini pada dasarnya adalah tujuan dari pengetikan yang kuat.

Jika beberapa pemrogram yang merancang metode async di toko Anda tidak dapat membuat keputusan berdasarkan informasi, mungkin akan membantu jika menugaskan mentor senior untuk masing-masing pemrogram yang kurang berpengalaman dan melakukan peninjauan kode mingguan. Jika mereka salah menebak, jelaskan mengapa itu harus dilakukan secara berbeda. Ini overhead untuk orang-orang senior, tapi itu akan membuat junior lebih cepat lebih cepat daripada hanya melemparkan mereka di ujung yang dalam dan memberi mereka beberapa aturan sewenang-wenang untuk diikuti.

Jika orang yang menulis metode itu tidak tahu apakah itu bisa disebut secara serempak, siapa di Bumi yang melakukannya ?!

Jika Anda memiliki banyak programmer yang tidak berpengalaman yang menulis metode async, apakah mereka juga memanggil mereka? Apakah mereka memenuhi syarat untuk mencari tahu sendiri mana yang aman untuk disebut async, atau akankah mereka mulai menerapkan aturan arbitrer yang serupa dengan cara mereka menyebut hal-hal ini?

Masalahnya di sini bukan tipe pengembalian Anda, itu adalah programer yang dimasukkan ke dalam peran yang belum siap. Itu pasti terjadi karena suatu alasan, jadi saya yakin itu tidak mudah untuk diperbaiki. Menggambarkannya tentu bukan solusi. Tetapi mencari cara untuk menyelinap masalah melewati kompiler juga bukan solusi.

15ee8f99-57ff-4f92-890c-b56153
sumber
4
Jika saya melihat metode kembali ValueTask<T>, saya berasumsi bahwa orang yang menulis metode melakukannya karena metode tersebut sebenarnya menggunakan fungsi yang ValueTask<T>menambahkan. Saya tidak mengerti mengapa Anda pikir semua metode Anda diinginkan untuk memiliki tipe pengembalian yang sama. Apa tujuannya di sana?
15ee8f99-57ff-4f92-890c-b56153
2
Sasaran 1 adalah untuk memungkinkan perubahan implementasi. Bagaimana jika saya tidak melakukan caching hasilnya sekarang tetapi menambahkan kode caching nanti? Sasaran 2 adalah memiliki pedoman yang jelas untuk tim di mana tidak semua anggota memahami kapan ValueTask berguna. Lakukan saja selalu jika tidak ada salahnya menggunakannya.
Stilgar
6
Dalam pandangan saya, itu hampir seperti memiliki semua metode Anda kembali objectkarena mungkin suatu hari nanti Anda akan ingin mereka untuk kembali intbukan string.
15ee8f99-57ff-4f92-890c-b56153
3
Mungkin tetapi jika Anda mengajukan pertanyaan "mengapa Anda pernah mengembalikan sebuah string, bukan objek" Saya dapat dengan mudah menjawabnya menunjuk pada kurangnya keamanan jenis. Alasan untuk tidak menggunakan ValueTask di mana-mana tampaknya jauh lebih halus.
Stilgar
3
@ Stilgar Satu hal yang akan saya katakan: Masalah halus seharusnya membuat Anda lebih khawatir daripada yang sudah jelas.
15ee8f99-57ff-4f92-890c-b56153
20

Ada beberapa perubahan di .Net Core 2.1 . Mulai dari .net core 2.1 ValueTask tidak hanya dapat mewakili tindakan yang disinkronkan yang diselesaikan tetapi juga async yang diselesaikan. Selain itu kami menerima jenis non-generik ValueTask.

Saya akan meninggalkan komentar Stephen Toub yang terkait dengan pertanyaan Anda:

Kami masih perlu memformalkan panduan, tetapi saya berharap ini akan menjadi sesuatu seperti ini untuk area permukaan API publik:

  • Tugas memberikan paling banyak kegunaan.

  • ValueTask menyediakan opsi terbanyak untuk optimalisasi kinerja.

  • Jika Anda menulis metode antarmuka / virtual yang akan ditimpa orang lain, ValueTask adalah pilihan default yang tepat.

  • Jika Anda mengharapkan API untuk digunakan di jalur panas di mana alokasi akan menjadi masalah, ValueTask adalah pilihan yang baik.

  • Kalau tidak, di mana kinerja tidak kritis, default untuk Tugas, karena memberikan jaminan dan kegunaan yang lebih baik.

Dari perspektif implementasi, banyak instance ValueTask yang dikembalikan masih akan didukung oleh Tugas.

Fitur dapat digunakan tidak hanya di .net core 2.1. Anda akan dapat menggunakannya dengan paket System.Threading.Tasks.Extensions .

unsafePtr
sumber
1
Lebih banyak dari Stephen hari ini: blogs.msdn.microsoft.com/dotnet/2018/11/07/...
Mike Cheel