Halo, Habr! Kami terus berbicara tentang pemrograman asinkron dalam C #. Hari ini kita akan berbicara tentang kasus penggunaan tunggal atau skenario khusus pengguna yang cocok untuk tugas apa pun dalam rangka pemrograman asinkron. Kami akan menyentuh pada topik sinkronisasi, deadlock, pengaturan operator, penanganan pengecualian, dan banyak lagi. Bergabunglah sekarang!

Artikel Terkait Sebelumnya
Hampir setiap perilaku non-standar dari metode asinkron dalam C # dapat dijelaskan berdasarkan satu skenario pengguna: mengonversi kode sinkron yang ada ke asinkron harus sesederhana mungkin. Anda harus dapat menambahkan kata kunci async sebelum kembali jenis metode, menambahkan akhiran Async ke nama metode ini, dan menambahkan kata kunci tunggu di sini dan di area teks metode untuk mendapatkan metode asinkron berfungsi penuh.

Skenario โsederhanaโ secara dramatis mengubah banyak aspek perilaku metode asinkron: dari merencanakan durasi tugas hingga menangani pengecualian. Script terlihat meyakinkan dan signifikan, tetapi dalam konteksnya, kesederhanaan metode asinkron menjadi sangat menyesatkan.
Sinkronkan konteks
Pengembangan User Interface (UI) adalah salah satu area di mana skenario di atas sangat penting. Karena operasi yang panjang di utas antarmuka pengguna, waktu respons aplikasi meningkat, dalam hal ini pemrograman asinkron selalu dianggap sebagai alat yang sangat efektif.
private async void buttonOk_ClickAsync(object sender, EventArgs args) { textBox.Text = "Running..";
Kode terlihat sangat sederhana, tetapi ada satu masalah. Ada batasan untuk sebagian besar antarmuka pengguna: Elemen UI hanya dapat diubah oleh utas khusus. Yaitu, di baris 3 kesalahan terjadi jika durasi tugas dijadwalkan di utas dari kumpulan utas. Untungnya, masalah ini sudah lama diketahui, dan konsep
konteks sinkronisasi telah muncul di .NET Framework 2.0.
Setiap UI menyediakan utilitas khusus untuk mengatur tugas menjadi satu atau lebih utas antarmuka pengguna khusus. Formulir Windows menggunakan metode
Control.Invoke
, WPF
Control.Invoke
metode Dispatcher.Invoke, sistem lain dapat mengakses metode lain. Skema yang digunakan dalam semua kasus ini sebagian besar serupa, tetapi berbeda dalam detailnya. Konteks sinkronisasi memungkinkan Anda untuk abstrak dari perbedaan dengan menyediakan API untuk menjalankan kode dalam konteks "khusus" yang menyediakan pemrosesan detail kecil dengan tipe turunan seperti
WindowsFormsSynchronizationContext
,
DispatcherSynchronizationContext
, dll.
Untuk mengatasi masalah afinitas utas, programmer C # memutuskan untuk memasukkan konteks sinkronisasi saat ini pada tahap awal implementasi metode asinkron dan merencanakan semua operasi selanjutnya dalam konteks ini. Sekarang, setiap blok antara menunggu pernyataan dieksekusi di utas antarmuka pengguna, yang memungkinkan untuk mengimplementasikan skrip utama. Namun, solusi ini memunculkan sejumlah masalah baru.
Jalan buntu
Mari kita lihat sepotong kecil kode yang relatif sederhana. Apakah ada masalah di sini?
Kode ini menyebabkan
jalan buntu . Utas antarmuka pengguna memulai operasi asinkron dan menunggu hasilnya secara sinkron. Namun, metode asinkron tidak dapat diselesaikan karena baris kedua
GetStockPricesForAsync
harus dieksekusi di utas antarmuka pengguna yang menyebabkan kebuntuan.
Anda akan keberatan bahwa masalah ini cukup mudah dipecahkan. Ya memang. Anda harus melarang semua panggilan ke metode
Task.Result
atau
Task.Wait
dari kode antarmuka pengguna, namun masalah masih dapat terjadi jika komponen yang digunakan oleh kode tersebut menunggu hasil operasi pengguna secara sinkron:
Kode ini lagi menyebabkan kebuntuan. Cara mengatasinya:
- Anda seharusnya tidak memblokir kode asinkron dengan
Task.Wait()
atau Task.Result
dan - gunakan
ConfigureAwait(false)
dalam kode perpustakaan.
Arti dari rekomendasi pertama jelas, dan yang kedua akan kami jelaskan di bawah ini.
Mengkonfigurasi menunggu pernyataan
Ada dua alasan mengapa kebuntuan terjadi dalam contoh terakhir:
Task.Wait()
di
GetStockPricesForAsync
dan penggunaan tidak langsung dari konteks sinkronisasi dalam langkah-langkah selanjutnya dalam InitializeIfNeededAsync. Meskipun pemrogram C # tidak merekomendasikan pemblokiran panggilan ke metode asinkron, jelas bahwa dalam kebanyakan kasus pemblokiran ini masih digunakan. Pemrogram C # menawarkan solusi berikut untuk masalah kebuntuan:
Task.ConfigureAwait(continueOnCapturedContext:false)
.
Terlepas dari penampilan yang aneh (jika pemanggilan metode dieksekusi tanpa argumen bernama, ini tidak berarti apa-apa sama sekali), solusi ini menjalankan fungsinya: ia memberikan kelanjutan eksekusi yang dipaksakan tanpa konteks sinkronisasi.
public Task<decimal> GetStockPricesForAsync(string symbol) { InitializeIfNeededAsync().Wait(); return Task.FromResult((decimal)42); } private async Task InitializeIfNeededAsync() => await Task.Delay(1).ConfigureAwait(false);
Dalam hal ini, kelanjutan tugas
Task.Delay(1
) (di sini adalah pernyataan kosong) direncanakan di utas dari kumpulan utas, dan bukan di utas antarmuka pengguna, yang menghilangkan kebuntuan.
Menonaktifkan konteks sinkronisasi
Saya tahu bahwa
ConfigureAwait
sebenarnya memecahkan masalah ini, tetapi memunculkan lebih banyak. Ini adalah contoh kecil:
public Task<decimal> GetStockPricesForAsync(string symbol) { InitializeIfNeededAsync().Wait(); return Task.FromResult((decimal)42); } private async Task InitializeIfNeededAsync() {
Apakah Anda melihat masalahnya? Kami menggunakan
ConfigureAwait(false)
, jadi semuanya harus baik-baik saja. Tapi bukan fakta.
ConfigureAwait(false)
mengembalikan objek custom awaiter
ConfiguredTaskAwaitable
, dan kami tahu bahwa itu hanya digunakan jika tugas tidak selesai secara sinkron. Yaitu, jika
_cache.InitializeAsync()
selesai secara sinkron, jalan buntu masih mungkin terjadi.
Untuk menghilangkan kebuntuan, semua tugas yang menunggu penyelesaian harus "dihiasi" dengan panggilan ke metode
ConfigureAwait(false)
. Semua ini mengganggu dan menghasilkan kesalahan.
Atau, Anda dapat menggunakan objek kustom penunggu di semua metode publik untuk menonaktifkan konteks sinkronisasi dalam metode asinkron:
private void buttonOk_Click(object sender, EventArgs args) { textBox.Text = "Running.."; var result = _stockPrices.GetStockPricesForAsync("MSFT").Result; textBox.Text = "Result is: " + result; }
Awaiters.DetachCurrentSyncContext
mengembalikan objek penunggu kustom berikut:
public struct DetachSynchronizationContextAwaiter : ICriticalNotifyCompletion {
DetachSynchronizationContextAwaiter
melakukan hal berikut: metode async bekerja dengan konteks sinkronisasi non-nol. Tetapi jika metode async bekerja tanpa konteks sinkronisasi, properti
IsCompleted
mengembalikan true, dan kelanjutan metode dilakukan secara sinkron.
Ini berarti bahwa data layanan mendekati nol ketika metode asinkron dijalankan dari utas di utas, dan pembayaran dilakukan sekali untuk mentransfer eksekusi dari utas antarmuka pengguna ke utas dari utas dari utas.
Manfaat lain dari pendekatan ini tercantum di bawah ini.
- Kemungkinan kesalahan berkurang.
ConfigureAwait(false)
hanya berfungsi jika diterapkan pada semua tugas yang menunggu penyelesaian. Perlu dilupakan setidaknya satu hal - dan kebuntuan dapat terjadi. Dalam kasus objek penunggu kustom, ingat bahwa semua metode perpustakaan umum harus dimulai dengan Awaiters.DetachCurrentSyncContext()
. Kesalahan mungkin terjadi di sini, tetapi kemungkinannya jauh lebih rendah. - Kode yang dihasilkan lebih deklaratif dan jelas. Metode
ConfigureAwait
dengan beberapa panggilan tampaknya kurang dapat dibaca oleh saya (karena elemen tambahan) dan tidak cukup informatif untuk pemula.
Penanganan pengecualian
Apa perbedaan antara dua opsi ini:
Task mayFail = Task.FromException (ArgumentNullException baru ());
Dalam kasus pertama, semuanya memenuhi harapan - pemrosesan kesalahan dilakukan, tetapi dalam kasus kedua ini tidak terjadi. Pustaka tugas paralel TPL dirancang untuk pemrograman asinkron dan paralel, dan Tugas / Tugas dapat mewakili hasil beberapa operasi. Itulah sebabnya
Task.Result
dan
Task.Wait()
selalu melempar
AggregateException
, yang mungkin mengandung beberapa kesalahan.
Namun, skenario utama kami mengubah segalanya: pengguna harus dapat menambahkan operator async / menunggu tanpa menyentuh logika penanganan kesalahan. Artinya, pernyataan menunggu harus berbeda dari
Task.Result
/
Task.Wait()
: itu harus mengambil pembungkus satu pengecualian dalam contoh
AggregateException
. Hari ini kita akan memilih pengecualian pertama.
Semuanya baik-baik saja jika semua metode berdasarkan Tugas tidak sinkron dan komputasi paralel tidak digunakan untuk melakukan tugas. Tetapi dalam beberapa kasus, semuanya berbeda:
try { Task<int> task1 = Task.FromException<int>(new ArgumentNullException()); Task<int> task2 = Task.FromException<int>(new InvalidOperationException());
Task.WhenAll
mengembalikan tugas dengan dua kesalahan, pernyataan menunggu menunggu dan hanya mengisi yang pertama.
Ada dua cara untuk mengatasi masalah ini:
- melihat tugas secara manual jika mereka memiliki akses, atau
- mengkonfigurasi pustaka TPL untuk memaksa pengecualian untuk dibungkus dengan
AggregateException
lain.
try { Task<int> task1 = Task.FromException<int>(new ArgumentNullException()); Task<int> task2 = Task.FromException<int>(new InvalidOperationException());
Metode batal async
Metode berbasis tugas mengembalikan token yang dapat digunakan untuk memproses hasil di masa depan. Jika tugas tersebut hilang, token menjadi tidak dapat diakses untuk dibaca oleh kode pengguna. Operasi asinkron yang mengembalikan metode void melempar kesalahan yang tidak dapat ditangani dalam kode pengguna. Dalam hal ini, token tidak berguna dan bahkan berbahaya - sekarang kita akan melihatnya. Namun, skenario utama kami mengasumsikan penggunaan wajib mereka:
private async void buttonOk_ClickAsync(object sender, EventArgs args) { textBox.Text = "Running.."; var result = await _stockPrices.GetStockPricesForAsync("MSFT"); textBox.Text = "Result is: " + result; }
Tetapi bagaimana jika
GetStockPricesForAsync
melakukan kesalahan? Pengecualian metode async void yang tidak ditangani dimasukkan ke dalam konteks sinkronisasi saat ini, memicu perilaku yang sama seperti untuk kode sinkron (untuk informasi lebih lanjut, lihat
Metode ThrowAsync pada halaman web
AsyncMethodBuilder.cs ). Pada Windows Forms, pengecualian yang tidak ditangani dalam event handler memecat
Application.ThreadException
AcaraThreadException, untuk WPF,
Application.DispatcherUnhandledException
acara kebakaran, dan sebagainya.
Bagaimana jika metode void async tidak mendapatkan konteks sinkronisasi? Dalam hal ini, pengecualian yang tidak ditangani menyebabkan crash aplikasi yang fatal. Itu tidak akan memecat acara [
TaskScheduler.UnobservedTaskException
] yang akan dipulihkan, tetapi itu akan memecat
AppDomain.UnhandledException
acara yang tidak dapat dipulihkan dan kemudian menutup aplikasi. Ini terjadi dengan sengaja, dan inilah hasil yang kita butuhkan.
Sekarang mari kita lihat cara terkenal lainnya: menggunakan metode batal asinkron hanya untuk penangan peristiwa antarmuka pengguna.
Sayangnya, metode void asynch mudah dipanggil secara tidak sengaja.
public static Task<T> ActionWithRetry<T>(Func<Task<T>> provider, Action<Exception> onError) {
Pada pandangan pertama, ekspresi lambda sulit untuk mengatakan apakah fungsinya adalah metode berbasis tugas atau metode batal async, dan oleh karena itu kesalahan dapat merayap ke dalam basis kode Anda, meskipun ada pemeriksaan paling teliti.
Kesimpulan
Banyak aspek pemrograman asinkron dalam C # dipengaruhi oleh skenario pengguna tunggal - cukup dengan mengubah kode sinkron aplikasi antarmuka pengguna yang ada menjadi asinkron:
- Eksekusi selanjutnya dari metode asinkron dijadwalkan dalam konteks sinkronisasi yang dihasilkan, yang dapat menyebabkan kebuntuan.
- Untuk mencegahnya, perlu dilakukan panggilan
ConfigureAwait(false)
di mana-mana dalam kode pustaka asinkron. - menunggu tugas; menghasilkan kesalahan pertama, dan ini mempersulit penciptaan pengecualian pemrosesan untuk pemrograman paralel.
- Metode batal async telah diperkenalkan untuk menangani peristiwa antarmuka pengguna, tetapi mereka mudah dijalankan secara tidak sengaja, yang akan menyebabkan aplikasi mogok jika pengecualian dilemparkan.
Keju gratis hanya terjadi di perangkap tikus. Kemudahan penggunaan kadang-kadang dapat menyebabkan kesulitan besar di bidang lain. Jika Anda terbiasa dengan sejarah pemrograman asinkron dalam C #, perilaku paling aneh tidak lagi tampak aneh, dan kemungkinan kesalahan dalam kode asinkron berkurang secara signifikan.