Fungsi Async / Await diperkenalkan di C # 5 untuk meningkatkan respons antarmuka pengguna dan akses web ke sumber daya. Dengan kata lain, metode asinkron membantu pengembang melakukan operasi asinkron yang tidak memblokir utas dan mengembalikan hasil skalar tunggal. Setelah berbagai upaya oleh Microsoft untuk menyederhanakan operasi asinkron, templat async / menunggu telah mendapatkan reputasi yang baik di antara para pengembang berkat pendekatan yang sederhana.
Metode asinkron yang ada sangat terbatas karena mereka hanya mengembalikan satu nilai. Mari kita lihat beberapa async Task<int> DoAnythingAsync()
metode yang umum untuk sintaksis seperti itu. Hasil karyanya adalah makna seseorang. Karena batasan ini, Anda tidak dapat menggunakan fungsi ini dengan kata kunci yield
dan antarmuka IEnumerable<int>
sinkron (untuk mengembalikan hasil enumerasi asinkron).

Jika Anda menggabungkan fungsi async/await
dan yield
, maka Anda dapat menggunakan model pemrograman yang kuat yang dikenal sebagai tarikan data asinkron , atau enumerasi enumerasi berbasis tarikan, atau urutan asinkron asinkron , seperti yang disebut dalam F #.
Kemampuan baru untuk menggunakan utas sinkron di C # 8 menghilangkan batasan yang terkait dengan mengembalikan hasil tunggal dan memungkinkan metode asinkron untuk mengembalikan beberapa nilai. Perubahan ini akan memberikan Templat asinkron lebih banyak fleksibilitas, dan pengguna akan dapat mengambil data dari suatu tempat (misalnya, dari database) menggunakan urutan asinkron tertunda atau untuk menerima data dari urutan asinkron di bagian yang tersedia.
Contoh:
foreach await (var streamChunck in asyncStreams) { Console.WriteLine($βReceived data count = {streamChunck.Count}β); }
Pendekatan lain untuk memecahkan masalah yang berkaitan dengan pemrograman asinkron adalah dengan menggunakan ekstensi reaktif (Rx). Rx
semakin penting di antara pengembang dan metode ini digunakan dalam banyak bahasa pemrograman, misalnya Java (RxJava) dan JavaScript (RxJS).
Rx didasarkan pada model push push (prinsip Tell Don't Ask), juga dikenal sebagai pemrograman reaktif. Yaitu tidak seperti IEnumerable, ketika konsumen meminta elemen berikutnya, dalam model Rx, penyedia data memberi sinyal kepada konsumen bahwa elemen baru muncul dalam urutan. Data didorong ke antrian dalam mode asinkron dan konsumen menggunakannya pada saat penerimaan.
Pada artikel ini, saya akan membandingkan model berdasarkan mendorong data (seperti Rx) dengan model berdasarkan menarik data (seperti IEnumerable), dan juga menunjukkan skenario mana yang paling cocok untuk model mana. Seluruh konsep dan manfaat diperiksa dengan berbagai contoh dan kode demo. Pada akhirnya, saya akan menunjukkan aplikasi dan menunjukkannya dengan contoh kode.
Perbandingan model berdasarkan mendorong data dengan model berdasarkan menarik data (pull-)

Fig. -1- Perbandingan model berdasarkan penarikan data dengan model berdasarkan mendorong data
Contoh-contoh ini didasarkan pada hubungan antara penyedia data dan konsumen, seperti yang ditunjukkan pada Gambar. -1-. Model berbasis tarikan mudah dimengerti. Di dalamnya, konsumen meminta dan menerima data dari pemasok. Pendekatan alternatif adalah model push push. Di sini, penyedia mempublikasikan data dalam antrian dan konsumen harus berlangganan untuk menerimanya.
Model tarikan data cocok untuk kasus-kasus di mana penyedia menghasilkan data lebih cepat daripada konsumen menggunakannya. Dengan demikian, konsumen hanya menerima data yang diperlukan, yang menghindari masalah luapan. Jika konsumen menggunakan data lebih cepat dari yang dihasilkan oleh pemasok, model yang sesuai dengan mendorong data itu cocok. Dalam hal ini, pemasok dapat mengirim lebih banyak data ke konsumen sehingga tidak ada penundaan yang tidak perlu.
Rx dan Akka Streams (model pemrograman berbasis aliran) menggunakan metode tekanan balik untuk mengontrol aliran. Untuk memecahkan masalah pemasok dan penerima yang dijelaskan di atas, metode ini menggunakan mendorong dan menarik data.
Pada contoh di bawah ini, konsumen yang lambat menarik data dari penyedia yang lebih cepat. Setelah konsumen memproses elemen saat ini, ia akan meminta pemasok untuk selanjutnya dan seterusnya sampai akhir urutan.
Untuk memahami seluruh kebutuhan utas asinkron, pertimbangkan kode berikut.
// (count) static int SumFromOneToCount(int count) { ConsoleExt.WriteLine("SumFromOneToCount called!"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; } return sum; } // : const int count = 5; ConsoleExt.WriteLine($"Starting the application with count: {count}!"); ConsoleExt.WriteLine("Classic sum starting."); ConsoleExt.WriteLine($"Classic sum result: {SumFromOneToCount(count)}"); ConsoleExt.WriteLine("Classic sum completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Kesimpulan:

Kita dapat membuat metode ditangguhkan menggunakan pernyataan hasil, seperti yang ditunjukkan di bawah ini.
static IEnumerable<int> SumFromOneToCountYield(int count) { ConsoleExt.WriteLine("SumFromOneToCountYield called!"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; yield return sum; } }
Panggilan metode
const int count = 5; ConsoleExt.WriteLine("Sum with yield starting."); foreach (var i in SumFromOneToCountYield(count)) { ConsoleExt.WriteLine($"Yield sum: {i}"); } ConsoleExt.WriteLine("Sum with yield completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Kesimpulan:

Seperti yang ditunjukkan pada jendela output di atas, hasilnya dikembalikan dalam bagian-bagian, bukan nilai tunggal. Ringkasan hasil yang ditunjukkan di atas dikenal sebagai daftar ditangguhkan. Namun, masalahnya masih belum terpecahkan: metode penjumlahan memblokir kode. Jika Anda melihat utas, Anda dapat melihat bahwa semuanya berjalan di utas utama.
Mari kita terapkan kata ajaib async ke metode SumFromOneToCount pertama (tanpa hasil).
static async Task<int> SumFromOneToCountAsync(int count) { ConsoleExt.WriteLine("SumFromOneToCountAsync called!"); var result = await Task.Run(() => { var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; } return sum; }); return result; }
Panggilan metode
const int count = 5; ConsoleExt.WriteLine("async example starting."); // . , . , . var result = await SumFromOneToCountAsync(count); ConsoleExt.WriteLine("async Result: " + result); ConsoleExt.WriteLine("async completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Kesimpulan:

Bagus Sekarang perhitungan dilakukan di utas yang berbeda, tetapi masalah dengan hasil masih ada. Sistem mengembalikan hasilnya dengan nilai tunggal.
Bayangkan bahwa kita dapat menggabungkan enumerasi yang ditangguhkan (pernyataan hasil) dan metode asinkron dalam gaya pemrograman imperatif. Kombinasi ini disebut stream asinkron dan ini adalah fitur baru dalam C # 8. Ini bagus untuk menyelesaikan masalah yang terkait dengan model pemrograman berdasarkan ekstraksi data, misalnya, mengunduh data dari situs atau membaca catatan dalam file atau database dengan cara modern.
Mari kita coba melakukan ini dalam versi C # saat ini. Saya akan menambahkan kata kunci async ke metode SumFromOneToCountYield sebagai berikut:

Fig. -2- Kesalahan saat menggunakan kata kunci hasil dan async secara bersamaan.
Ketika kami mencoba menambahkan async ke SumFromOneToCountYield, kesalahan terjadi seperti yang ditunjukkan di atas.
Mari kita coba secara berbeda. Kami dapat menghapus kata kunci hasil dan menerapkan IEnumerable dalam tugas, seperti yang ditunjukkan di bawah ini:
static async Task<IEnumerable<int>> SumFromOneToCountTaskIEnumerable(int count) { ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable called!"); var collection = new Collection<int>(); var result = await Task.Run(() => { var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; collection.Add(sum); } return collection; }); return result; }
Panggilan metode
const int count = 5; ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable started!"); var scs = await SumFromOneToCountTaskIEnumerable(count); ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable done!"); foreach (var sc in scs) { // , . . ConsoleExt.WriteLine($"AsyncIEnumerable Result: {sc}"); } ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
Kesimpulan:

Seperti yang Anda lihat dari contoh, semuanya dihitung dalam mode asinkron, tetapi masalahnya masih ada. Hasil (semua hasil dikumpulkan dalam koleksi) dikembalikan sebagai satu blok. Dan ini bukan yang kita butuhkan. Jika Anda ingat, tujuan kami adalah untuk menggabungkan mode perhitungan asinkron dengan kemungkinan penundaan.
Untuk melakukan ini, Anda perlu menggunakan perpustakaan eksternal, misalnya, Ix (bagian dari Rx), atau utas asinkron, disajikan dalam C #.
Mari kita kembali ke kode kita. Untuk menunjukkan perilaku asinkron, saya menggunakan perpustakaan eksternal .
static async Task ConsumeAsyncSumSeqeunc(IAsyncEnumerable<int> sequence) { ConsoleExt.WriteLineAsync("ConsumeAsyncSumSeqeunc Called"); await sequence.ForEachAsync(value => { ConsoleExt.WriteLineAsync($"Consuming the value: {value}"); // Task.Delay(TimeSpan.FromSeconds(1)).Wait(); }); } static IEnumerable<int> ProduceAsyncSumSeqeunc(int count) { ConsoleExt.WriteLineAsync("ProduceAsyncSumSeqeunc Called"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; // Task.Delay(TimeSpan.FromSeconds(0,5)).Wait(); yield return sum; } }
Panggilan metode
const int count = 5; ConsoleExt.WriteLine("Starting Async Streams Demo!"); // . . IAsyncEnumerable<int> pullBasedAsyncSequence = ProduceAsyncSumSeqeunc(count).ToAsyncEnumerable(); ConsoleExt.WriteLineAsync("X#X#X#X#X#X#X#X#X#X# Doing some other work X#X#X#X#X#X#X#X#X#X#"); // ; . var consumingTask = Task.Run(() => ConsumeAsyncSumSeqeunc(pullBasedAsyncSequence)); // . , . consumingTask.Wait(); ConsoleExt.WriteLineAsync("Async Streams Demo Done!");
Kesimpulan:

Akhirnya, kita melihat perilaku yang diinginkan. Anda dapat menjalankan loop enumerasi dalam mode asinkron.
Lihat kode sumber di sini .
Contohnya, data yang tidak sinkron menggunakan arsitektur client-server
Mari kita lihat konsep ini dengan contoh yang lebih realistis. Semua manfaat fitur ini paling baik dilihat dalam konteks arsitektur client-server.
Panggilan sinkron jika arsitektur klien-server
Saat mengirim permintaan ke server, klien dipaksa untuk menunggu (mis., Diblokir) hingga respons datang, seperti yang ditunjukkan pada Gambar. -3-.

Fig. -3- Menarik data sinkron, di mana klien menunggu sampai pemrosesan permintaan selesai
Menarik data yang tidak sinkron
Dalam hal ini, klien meminta data dan beralih ke tugas lain. Setelah data diterima, klien akan terus melakukan pekerjaan.

Fig. -4- Menarik data yang tidak sinkron di mana klien dapat melakukan tugas-tugas lain saat data sedang diminta
Menarik data secara tidak sinkron
Dalam hal ini, klien meminta bagian dari data dan terus melakukan tugas-tugas lain. Kemudian, setelah menerima data, klien memprosesnya dan meminta bagian selanjutnya, dan seterusnya, sampai semua data diterima. Dari skenario inilah ide tentang thread asinkron muncul. Dalam gbr. -5- menunjukkan bagaimana klien dapat memproses data yang diterima atau melakukan tugas lain.

Fig. -5- Menarik data sebagai urutan asinkron (stream asinkron). Klien tidak diblokir.
Utas asinkron
Seperti IEnumerable<T>
dan IEnumerator<T>
ada dua antarmuka baru, IAsyncEnumerable<T>
dan IAsyncEnumerator<T>
, yang didefinisikan seperti yang ditunjukkan di bawah ini:
public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { Task<bool> MoveNextAsync(); T Current { get; } } // public interface IAsyncDisposable { Task DiskposeAsync(); }
Di InfoQ, Jonathan Allen memperbaiki topik ini. Di sini saya tidak akan masuk ke detail, jadi saya sarankan membaca artikelnya .
Fokusnya adalah pada nilai pengembalian Task<bool> MoveNextAsync()
(diubah dari bool menjadi Task<bool>
, bool IEnumerator.MoveNext()
). Berkat dia, semua perhitungan, serta iterasi mereka, akan terjadi secara serempak. Konsumen memutuskan kapan untuk mendapatkan nilai berikutnya. Meskipun model asinkron, masih menggunakan penarikan data. Untuk pembersihan sumber daya yang tidak sinkron, Anda dapat menggunakan antarmuka IAsyncDisposable
. Informasi lebih lanjut tentang utas sinkron dapat ditemukan di sini .
Sintaks
Sintaks terakhir akan terlihat seperti berikut ini:
foreach await (var dataChunk in asyncStreams) { // yield . }
Dari contoh di atas, jelas bahwa alih-alih menghitung nilai tunggal, kami, secara teoritis, dapat secara berurutan menghitung serangkaian nilai, sambil menunggu operasi asinkron lainnya.
Contoh Microsoft yang didesain ulang
Saya menulis ulang kode demo Microsoft. Itu dapat diunduh seluruhnya dari repositori GitHub saya .
Contoh ini didasarkan pada gagasan untuk menciptakan aliran besar dalam memori (array 20.000 byte) dan secara berurutan mengekstraksi elemen darinya dalam mode asinkron. Selama setiap iterasi, 8 KB ditarik dari array.


Pada langkah (1), array data besar dibuat, diisi dengan nilai dummy. Kemudian, selama langkah (2), variabel yang disebut checksum didefinisikan. Variabel yang mengandung checksum ini dimaksudkan untuk memverifikasi kebenaran jumlah penjumlahan. Array dan checksum dibuat dalam memori dan dikembalikan sebagai urutan elemen pada langkah (3).
Langkah (4) melibatkan penerapan AsEnumarble
ekstensi AsEnumarble
(nama yang lebih cocok AsAsyncEnumarble), yang membantu mensimulasikan aliran asinkron 8 KB (BufferSize = 8000 elemen (6))
Biasanya tidak diperlukan untuk mewarisi dari IAsyncEnumerable, tetapi dalam contoh yang ditunjukkan di atas, operasi ini dilakukan untuk menyederhanakan kode demo, seperti yang ditunjukkan pada langkah (5).
Langkah (7) melibatkan penggunaan kata kunci foreach
, yang mengekstrak 8 KB potongan data dari aliran asinkron dalam memori. Proses menarik terjadi secara berurutan: ketika konsumen (bagian dari kode yang berisi foreach
) siap menerima potongan data berikutnya, ia menariknya dari penyedia (array yang terkandung dalam aliran dalam memori). Akhirnya, ketika siklus selesai, program akan memeriksa nilai 'c' untuk checksum dan jika mereka cocok, itu akan menampilkan pesan "Checksums cocok!", Menurut langkah (8).
Jendela output demo Microsoft:

Kesimpulan
Kami melihat utas asinkron, yang bagus untuk menarik data secara asinkron dan menulis kode yang menghasilkan beberapa nilai dalam mode asinkron.
Menggunakan model ini, Anda dapat meminta elemen data berikutnya secara berurutan dan mendapatkan respons. Ini berbeda dari model push data IObservable<T>
, menggunakan nilai mana yang dihasilkan terlepas dari keadaan konsumen. Aliran asinkron memungkinkan Anda untuk secara sempurna mewakili sumber data asinkron yang dikendalikan oleh konsumen ketika ia sendiri menentukan kesediaan untuk menerima bagian data berikutnya. Contohnya termasuk menggunakan aplikasi web atau membaca catatan dalam database.
Saya mendemonstrasikan cara membuat enumerasi dalam mode asinkron dan menggunakannya menggunakan perpustakaan eksternal dengan urutan asinkron. Saya juga menunjukkan manfaat apa yang disediakan fitur ini saat mengunduh konten dari Internet. Akhirnya, kami melihat sintaks baru untuk utas asinkron, serta contoh lengkap penggunaannya berdasarkan Microsoft Build Demo Code ( 7β9 Mei, 2018 // Seattle, WA )
