Kata pengantar untuk terjemahan
Tidak seperti artikel ilmiah, artikel jenis ini sulit diterjemahkan "mendekati teks", dan cukup banyak adaptasi yang harus dilakukan. Untuk alasan ini, saya meminta maaf atas beberapa kebebasan, untuk bagian saya, dalam berurusan dengan teks artikel asli. Saya hanya dibimbing oleh satu tujuan - untuk membuat terjemahan dapat dimengerti, bahkan jika di tempat itu sangat menyimpang dari artikel asli. Saya akan berterima kasih atas kritik konstruktif dan koreksi / penambahan terjemahan.
Pendahuluan
System.Threading.Tasks
namespace dan kelas Task
pertama kali diperkenalkan di .NET Framework 4. Sejak saat itu, tipe ini, dan class turunannya Task<TResult>
, telah dengan kuat memasuki praktik pemrograman di .NET dan telah menjadi aspek kunci dari model asinkron. diimplementasikan dalam C # 5, dengan async/await
. Pada artikel ini, saya akan berbicara tentang tipe-tipe baru ValueTask/ValueTask<TResult>
yang diperkenalkan untuk meningkatkan kinerja kode asinkron, dalam kasus di mana overhead memori memainkan peran kunci.

Tugas
Task
melayani beberapa tujuan, tetapi yang utama adalah "janji" - objek yang mewakili kemampuan untuk menunggu penyelesaian operasi. Anda memulai operasi dan mendapatkan Task
. Task
ini akan selesai ketika operasi itu sendiri selesai. Dalam hal ini, ada tiga opsi:
- Operasi selesai secara sinkron di utas inisiator. Misalnya, ketika mengakses beberapa data yang sudah ada di buffer .
- Operasi dilakukan secara tidak sinkron, tetapi berhasil diselesaikan pada saat inisiator menerima
Task
. Misalnya, ketika melakukan akses cepat ke data yang belum di-buffer - Operasi dilakukan secara tidak sinkron, dan berakhir setelah inisiator menerima
Task
contohnya adalah penerimaan data melalui jaringan .
Untuk mendapatkan hasil panggilan asinkron, klien dapat memblokir utas panggilan sambil menunggu penyelesaian, yang sering bertentangan dengan ide asinkron, atau menyediakan metode panggilan balik yang akan dieksekusi setelah penyelesaian operasi asinkron. Model panggilan balik di .NET 4 disajikan secara eksplisit, menggunakan metode ContinueWith
dari objek kelas Task
, yang menerima delegasi yang dipanggil setelah penyelesaian operasi asinkron.
SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } });
Dengan .NET Frmaework 4.5 dan C # 5, mendapatkan hasil dari operasi asinkron disederhanakan dengan memperkenalkan kata kunci async/await
dan mekanisme di baliknya. Mekanisme ini, kode yang dihasilkan, mampu mengoptimalkan semua kasus yang disebutkan di atas, dengan benar menangani penyelesaian terlepas dari jalan di mana ia tercapai.
TResult result = await SomeOperationAsync(); UseResult(result);
Kelas Task
cukup fleksibel dan memiliki beberapa keunggulan. Misalnya, Anda dapat "mengharapkan" objek kelas ini beberapa kali, Anda dapat mengharapkan hasilnya secara kompetitif, oleh sejumlah konsumen. Contoh kelas dapat disimpan dalam kamus untuk sejumlah panggilan berikutnya, dengan tujuan "menunggu" di masa depan. Skenario yang dijelaskan memungkinkan Anda untuk mempertimbangkan objek Task
sebagai jenis cache hasil yang diperoleh secara tidak sinkron. Selain itu, Task
menyediakan kemampuan untuk memblokir utas tunggu sampai operasi selesai jika skrip mengharuskannya. Ada juga yang disebut. kombinator untuk berbagai strategi untuk menunggu penyelesaian set tugas, misalnya, "Task.WhenAny" - secara asinkron menunggu penyelesaian dari banyak tugas pertama.
Tetapi, bagaimanapun, kasus penggunaan yang paling umum hanyalah memulai operasi asinkron dan kemudian menunggu hasil dari eksekusi. Kasus sederhana seperti itu, sangat umum, tidak memerlukan fleksibilitas di atas:
TResult result = await SomeOperationAsync(); UseResult(result);
Ini sangat mirip dengan cara kami menulis kode sinkron (mis. TResult result = SomeOperation();
). Opsi ini secara alami diterjemahkan ke dalam async/await
.
Selain itu, untuk semua kelebihannya, jenis Task
memiliki kelemahan potensial. Task
adalah kelas, yang berarti bahwa setiap operasi yang membuat turunan tugas mengalokasikan objek pada heap. Semakin banyak objek yang kita buat, semakin banyak pekerjaan yang dibutuhkan dari GC, dan semakin banyak sumber daya yang dihabiskan untuk pekerjaan pengumpul sampah, sumber daya yang dapat digunakan untuk tujuan lain. Ini menjadi masalah yang jelas untuk kode, di mana, di satu sisi, instance Task
sering dibuat, dan di sisi lain, yang telah meningkatkan persyaratan untuk throughput dan kinerja.
Runtime dan pustaka utama, dalam banyak situasi, berhasil mengurangi efek ini. Misalnya, jika Anda menulis metode seperti di bawah ini:
public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; }
dan, paling sering, akan ada cukup ruang di buffer, operasi akan berakhir secara serempak. Jika demikian, maka tidak ada yang istimewa tentang tugas yang dikembalikan, tidak ada nilai balik, dan operasi sudah selesai. Dengan kata lain, kita berhadapan dengan Task
, setara dengan operasi void
sinkron. Dalam situasi seperti itu, runtime dengan mudah meng-cache objek Task
, dan menggunakannya setiap kali sebagai hasil untuk tugas async Task
- metode yang selesai secara sinkron ( Task.ComletedTask
). Contoh lain, katakanlah Anda menulis:
public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; }
Misalkan, dengan cara yang sama, bahwa dalam banyak kasus, ada beberapa data dalam buffer. Metode ini memeriksa _bufferedCount
, melihat bahwa variabel lebih besar dari nol, dan mengembalikan true
. Hanya jika pada saat verifikasi data tidak disangga, operasi asinkron diperlukan. Namun, hanya ada dua kemungkinan hasil logis ( true
dan false
), dan hanya dua kemungkinan kembali melalui Task<bool>
. Berdasarkan penyelesaian sinkron, atau asinkron, tetapi sebelum keluar dari metode, runtime cache dua instance dari Task<bool>
(satu untuk true
dan satu untuk false
), dan mengembalikan yang diinginkan, menghindari alokasi tambahan. Satu-satunya opsi ketika Anda harus membuat objek Task<bool>
adalah kasus eksekusi asinkron, yang berakhir setelah "kembali". Dalam hal ini, metode harus membuat objek Task<bool>
, karena pada saat keluar dari metode, hasil operasi belum diketahui. Objek yang dikembalikan harus unik, karena pada akhirnya akan menyimpan hasil operasi asinkron.
Ada contoh lain dari caching serupa dari runtime. Tetapi strategi seperti itu tidak berlaku di mana-mana. Sebagai contoh, metode:
public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; }
juga sering berakhir secara serempak. Tetapi, tidak seperti contoh sebelumnya, metode ini mengembalikan hasil integer yang memiliki sekitar empat miliar nilai yang mungkin. Untuk melakukan cache Task<int>
, dalam situasi ini, diperlukan ratusan gigabytes memori. Lingkungan di sini juga mendukung cache kecil untuk Task<int>
, untuk beberapa nilai kecil. Jadi, misalnya, jika operasi selesai secara sinkron (data ada di buffer), dengan hasil 4, cache akan digunakan. Tetapi jika hasilnya, meskipun sinkron, penyelesaiannya adalah 42, objek Task<int>
akan dibuat, mirip dengan memanggil Task.FromResult(42)
.
Banyak implementasi perpustakaan mencoba mengurangi situasi ini dengan mendukung cache mereka sendiri. Salah satu contoh adalah kelebihan MemoryStream.ReadAsync
. Operasi ini, diperkenalkan di .NET Framework 4.5, selalu berakhir secara sinkron, karena itu hanya pembacaan dari ingatan. ReadAsync
mengembalikan Task<int>
mana hasil integer mewakili jumlah byte yang dibaca. Cukup sering, dalam kode, situasi terjadi ketika ReadAsync
digunakan dalam satu lingkaran. Apalagi jika ada gejala-gejala berikut:
- Jumlah byte yang diminta tidak berubah untuk sebagian besar iterasi loop;
- Di sebagian besar iterasi,
ReadAsync
dapat membaca jumlah byte yang diminta.
Yaitu, untuk panggilan berulang, ReadAsync
berjalan secara sinkron dan mengembalikan objek Task<int>
, dengan hasil yang sama dari iterasi ke iterasi. Adalah logis bahwa MemoryStream
cache tugas terakhir yang berhasil diselesaikan, dan untuk semua panggilan berikutnya, jika hasil yang baru cocok dengan yang sebelumnya, ia akan mengembalikan instance dari cache. Jika hasilnya tidak cocok, maka Task.FromResult
digunakan untuk membuat contoh baru, yang, pada gilirannya, juga di-cache sebelum kembali.
Tetapi, bagaimanapun, ada banyak kasus ketika operasi dipaksa untuk membuat objek Task<TResult>
baru, bahkan ketika secara bersamaan diselesaikan.
ValueTask <TResult> dan penyelesaian sinkron
Semua ini, pada akhirnya, berfungsi sebagai motivasi untuk memperkenalkan tipe baru ValueTask<TResult>
ke .NET Core 2.0. Juga, melalui nuget-package System.Threading.Tasks.Extensions
, tipe ini tersedia di rilis .NET lainnya.
ValueTask<TResult>
diperkenalkan di .NET Core 2.0 sebagai struktur yang mampu membungkus TResult
atau Task<TResult>
. Ini berarti bahwa objek jenis ini dapat dikembalikan dari metode async
. Nilai tambah pertama dari pengenalan tipe ini langsung terlihat: jika metode berhasil diselesaikan dan disinkronkan, tidak perlu membuat sesuatu di heap, cukup membuat instance ValueTask<TResult>
dengan nilai hasil. Hanya jika metode keluar secara sinkron, kita perlu membuat Task<TResult>
. Dalam hal ini, ValueTask<TResult>
digunakan sebagai pembungkus lebih dari Task<TResult>
. Keputusan untuk membuat ValueTask<TResult>
dapat menggabungkan Task<TResult>
dibuat dengan tujuan pengoptimalan: jika berhasil dan jika gagal, metode asinkron menciptakan Task<TResult>
, dari sudut pandang optimasi memori, lebih baik menggabungkan Task<TResult>
objek Task<TResult>
itu sendiri Task<TResult>
daripada menyimpan bidang tambahan di ValueTask<TResult>
untuk berbagai kasus penyelesaian (misalnya, untuk menyimpan pengecualian).
Mengingat hal di atas, tidak ada lagi kebutuhan untuk caching dalam metode seperti MemoryStream.ReadAsync
atas, tetapi sebaliknya dapat diimplementasikan sebagai berikut:
public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count) { try { int bytesRead = Read(buffer, offset, count); return new ValueTask<int>(bytesRead); } catch (Exception e) { return new ValueTask<int>(Task.FromException<int>(e)); } }
ValueTask <TResult> dan Pemutusan Asinkron
Memiliki kemampuan untuk menulis metode asinkron yang tidak memerlukan alokasi memori tambahan untuk hasilnya, dengan penyelesaian yang sinkron, benar-benar merupakan nilai tambah yang besar. Seperti yang dinyatakan di atas, ini adalah tujuan utama untuk memperkenalkan tipe ValueTask<TResult>
dalam .NET Core 2.0. Semua metode baru yang diharapkan akan digunakan di "jalan panas" sekarang menggunakan ValueTask<TResult>
alih-alih Task<TResult>
sebagai tipe pengembalian. Misalnya, kelebihan baru metode ReadAsync
untuk Stream
, dalam .NET Core 2.1 (yang menggunakan Memory<byte>
alih-alih byte[]
sebagai parameter), mengembalikan instance ValueTask<int>
. Ini memungkinkan untuk secara signifikan mengurangi jumlah alokasi ketika bekerja dengan stream (sangat sering metode ReadAsync
selesai secara sinkron, seperti dalam contoh dengan MemoryStream
).
Namun, ketika mengembangkan layanan dengan bandwidth tinggi, di mana penghentian asinkron tidak jarang terjadi, kita perlu melakukan yang terbaik untuk menghindari alokasi tambahan.
Seperti disebutkan sebelumnya, dalam model async/await
, operasi apa pun yang selesai secara tidak sinkron harus mengembalikan objek unik untuk menunggu penyelesaian. Unik karena itu akan berfungsi sebagai saluran untuk melakukan panggilan balik. Perhatikan, bagaimanapun, bahwa konstruksi ini tidak mengatakan apa-apa tentang apakah objek menunggu yang dikembalikan dapat digunakan kembali setelah selesainya operasi asinkron. Jika suatu objek dapat digunakan kembali, maka API dapat mempertahankan kumpulan untuk jenis objek ini. Namun, dalam kasus ini, kumpulan ini tidak dapat mendukung akses bersamaan - objek dari kumpulan akan beralih dari status "selesai" ke status "tidak selesai" dan sebaliknya.
Untuk mendukung kemungkinan bekerja dengan kumpulan seperti itu, IValueTaskSource<TResult>
ditambahkan ke .NET Core 2.1, dan struktur ValueTask<TResult>
diperluas: sekarang objek jenis ini tidak hanya dapat membungkus objek TResult
atau Task<TResult>
, tetapi juga contoh dari IValueTaskSource<TResult>
. Antarmuka baru menyediakan fungsionalitas dasar yang memungkinkan objek ValueTask<TResult>
bekerja dengan IValueTaskSource<TResult>
dengan cara yang sama seperti dengan Task<TResult>
:
public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted( Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); }
GetStatus
dimaksudkan untuk digunakan di properti ValueTask<TResult>.IsCompleted/IsCompletedSuccessfully
- memungkinkan Anda untuk mengetahui apakah operasi selesai atau tidak (berhasil atau tidak). OnCompleted
digunakan dalam ValueTask<TResult>
untuk memicu panggilan balik. GetResult
digunakan untuk mendapatkan hasilnya, atau untuk melemparkan pengecualian.
Sebagian besar pengembang tidak mungkin pernah perlu berurusan dengan IValueTaskSource<TResult>
metode asinkron, ketika dikembalikan, sembunyikan di balik ValueTask<TResult>
. Antarmuka itu sendiri terutama ditujukan bagi mereka yang mengembangkan API berkinerja tinggi dan berupaya menghindari pekerjaan yang tidak perlu dengan sekelompok.
Di .NET Core 2.1, ada beberapa contoh API semacam ini. Yang paling terkenal dari ini adalah kelebihan baru dari Socket.ReceiveAsync
Socket.SendAsync
metode Socket.SendAsync
dan Socket.SendAsync
. Sebagai contoh:
public ValueTask<int> ReceiveAsync( Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default);
Objek tipe ValueTask<int>
digunakan sebagai nilai balik.
Jika metode keluar secara sinkron, maka ia mengembalikan ValueTask<int>
dengan nilai yang sesuai:
int result = β¦; return new ValueTask<int>(result);
Jika operasi selesai secara tidak sinkron, maka objek yang di-cache digunakan yang mengimplementasikan IValueTaskSource<TResult>
:
IValueTaskSource<int> vts = β¦; return new ValueTask<int>(vts);
Implementasi Socket
mendukung satu objek cache untuk menerima, dan satu untuk mengirim data, selama masing-masing digunakan tanpa persaingan (tidak, misalnya, pengiriman data kompetitif). Strategi ini mengurangi jumlah memori tambahan yang dialokasikan, bahkan dalam kasus eksekusi asinkron.
Optimalisasi Socket
dijelaskan dalam .NET Core 2.1 memiliki dampak positif pada kinerja NetworkStream
. Kelebihannya adalah metode ReadAsync
dari kelas Stream
:
public virtual ValueTask<int> ReadAsync( Memory<byte> buffer, CancellationToken cancellationToken);
hanya mendelegasikan pekerjaan ke metode Socket.ReceiveAsync
. Meningkatkan efisiensi metode soket, dalam hal bekerja dengan memori, meningkatkan efisiensi metode NetworkStream
.
ValueTask non-generik
Sebelumnya, saya mencatat beberapa kali bahwa tujuan asli ValueTask<T>
, dalam .NET Core 2.0, adalah untuk mengoptimalkan kasus penyelesaian metode yang sinkron dengan hasil "tidak kosong". Ini berarti bahwa tidak perlu ValueTask
diketik: dalam kasus penyelesaian sinkron, metode menggunakan singleton melalui properti Task.CompletedTask
, dan runtime untuk metode async Task
juga diterima secara implisit.
Tetapi, dengan munculnya kemampuan untuk menghindari alokasi yang tidak perlu dan dengan eksekusi asinkron, kebutuhan untuk ValueTask
diketik lagi menjadi relevan. Untuk alasan ini, di .NET Core 2.1, kami memperkenalkan ValueTask
dan IValueTaskSource
. Mereka adalah analog dari tipe generik yang sesuai, dan digunakan dengan cara yang sama, tetapi untuk metode dengan pengembalian kosong ( void
).
Terapkan IValueTaskSource / IValueTaskSource <T>
Sebagian besar pengembang tidak perlu mengimplementasikan antarmuka ini. Dan implementasi mereka bukanlah tugas yang mudah. Jika Anda memutuskan bahwa Anda perlu mengimplementasikannya sendiri, maka, di dalam .NET Core 2.1, ada beberapa implementasi yang dapat berfungsi sebagai contoh:
Untuk menyederhanakan tugas-tugas ini (implementasi IValueTaskSource / IValueTaskSource<T>
), kami berencana untuk memperkenalkan tipe ManualResetValueTaskSourceCore<TResult>
di .NET Core 3.0. Struktur ini akan merangkum semua logika yang diperlukan. Contoh ManualResetValueTaskSourceCore<TResult>
dapat digunakan di objek lain yang mengimplementasikan IValueTaskSource<TResult>
dan / atau IValueTaskSource
, dan mendelegasikan sebagian besar pekerjaan untuk itu. Anda dapat mempelajari lebih lanjut tentang ini di ttps: //github.com/dotnet/corefx/issues/32664.
Model yang benar untuk menggunakan ValueTasks
Bahkan pemeriksaan sepintas ValueTask
bahwa ValueTask
dan ValueTask<TResult>
lebih terbatas daripada Task
dan Task<TResult>
. Dan ini normal, bahkan diinginkan, karena tujuan utama mereka adalah menunggu selesainya eksekusi asinkron.
Secara khusus, keterbatasan signifikan muncul karena fakta bahwa ValueTask
dan ValueTask<TResult>
dapat menggabungkan objek yang dapat digunakan kembali. Secara umum, operasi berikut * TIDAK PERNAH harus dilakukan ketika menggunakan ValueTask
/ ValueTask<TResult>
* ( biarkan saya merumuskan kembali melalui "Never" *):
- Jangan pernah menggunakan objek
ValueTask
/ ValueTask<TResult>
berulang kali
Motivasi: Task
dan contoh Task<TResult>
tidak pernah berubah dari keadaan "selesai" ke keadaan "tidak lengkap", kita dapat menggunakannya untuk menunggu hasil sebanyak yang kita inginkan - setelah selesai kita akan selalu mendapatkan hasil yang sama. Sebaliknya, karena ValueTask
/ ValueTask<TResult>
, mereka dapat bertindak sebagai pembungkus objek yang digunakan kembali, yang berarti bahwa keadaan mereka dapat berubah, karena keadaan objek yang digunakan kembali berubah menurut definisi - untuk beralih dari "selesai" ke "tidak lengkap" dan sebaliknya.
- Jangan Pernah
ValueTask
/ ValueTask<TResult>
dalam mode kompetitif.
Motivasi: Objek yang dibungkus mengharapkan untuk bekerja hanya dengan satu panggilan balik, dari satu konsumen pada satu waktu, dan upaya untuk bersaing secara kompetitif dapat dengan mudah menyebabkan kondisi balapan dan kesalahan pemrograman yang halus. Harapan kompetitif, ini adalah salah satu opsi yang dijelaskan di atas beberapa harapan . Perhatikan bahwa Task
/ Task<TResult>
memungkinkan sejumlah harapan kompetitif.
- Jangan pernah gunakan
.GetAwaiter().GetResult()
sampai operasi selesai .
Motivasi: Implementasi IValueTaskSource
/ IValueTaskSource<TResult>
tidak boleh mendukung penguncian sampai operasi selesai. Memblokir, pada kenyataannya, mengarah pada kondisi balapan, tidak mungkin bahwa ini akan menjadi perilaku yang diharapkan dari pihak konsumen. Sementara Task
/ Task<TResult>
memungkinkan Anda untuk melakukan ini, dengan demikian memblokir utas panggilan sampai operasi selesai.
Tetapi bagaimana jika, bagaimanapun, Anda perlu melakukan salah satu operasi yang dijelaskan di atas, dan metode yang dipanggil mengembalikan instance dari ValueTask
/ ValueTask<TResult>
? Untuk kasus seperti itu, ValueTask
/ ValueTask<TResult>
menyediakan metode .AsTask()
. Dengan memanggil metode ini, Anda akan mendapatkan instance dari Task
/ Task<TResult>
, dan Anda sudah dapat melakukan operasi yang diperlukan dengannya. Menggunakan kembali objek asli setelah memanggil .AsTask()
tidak diperbolehkan .
: ValueTask
/ ValueTask<TResult>
, ( await
) (, .ConfigureAwait(false)
), .AsTask()
, ValueTask
/ ValueTask<TResult>
.
// Given this ValueTask<int>-returning method⦠public ValueTask<int> SomeValueTaskReturningMethodAsync(); ⦠// GOOD int result = await SomeValueTaskReturningMethodAsync(); // GOOD int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false); // GOOD Task<int> t = SomeValueTaskReturningMethodAsync().AsTask(); // WARNING ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); ... // storing the instance into a local makes it much more likely it'll be misused, // but it could still be ok // BAD: awaits multiple times ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: awaits concurrently (and, by definition then, multiple times) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: uses GetAwaiter().GetResult() when it's not known to be done ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult();
, "", , ( , ).
ValueTask
/ ValueTask<TResult>
, . , IsCompleted
true
, ( , ), β false
, IsCompletedSuccessfully
true
. " " , , , , , . await
/ .AsTask()
.Result
. , SocketsHttpHandler
.NET Core 2.1, .ReadAsync
, ValueTask<int>
. , , , . , .. . Karena , , , , :
int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } }
, .. ValueTask<int>
, .Result
, await
, .
API ValueTask / ValueTask<TResult>?
, . Task
/ ValueTask<TResult>
.
, Task
/ Task<TResult>
. , "" / , Task
/ Task<TResult>
. , , ValueTask<TResult>
Task<TResult>
: , , await
Task<TResult>
ValueTask<TResult>
. , (, API Task
Task<bool>
), , , Task
( Task<bool>
). , ValueTask
/ ValueTask<TResult>
. , async-, ValueTask
/ ValueTask<TResult>
, .
, ValueTask
/ ValueTask<TResult>
, :
- , API ,
- API ,
- , , , .
, abstract
/ virtual
, , / ?
Apa selanjutnya
.NET, API, Task
/ Task<TResult>
. , , API c ValueTask
/ ValueTask<TResult>
, . IAsyncEnumerator<T>
, .NET Core 3.0. IEnumerator<T>
MoveNext
, . β IAsyncEnumerator<T>
MoveNextAsync
. , Task<bool>
, , . , , , ( ), , , await foreach
-, , MoveNextAsync
, ValueTask<bool>
. , , , " " , . , C# , .
