ConfigureAwait: Pertanyaan yang Sering Diajukan

Halo, Habr! Saya hadir untuk Anda terjemahan artikel FAQ ConfigureAwait oleh Stephen Taub.

gambar

Async / await ditambahkan ke .NET lebih dari tujuh tahun yang lalu. Keputusan ini memiliki dampak signifikan tidak hanya pada ekosistem .NET - juga tercermin dalam banyak bahasa dan kerangka kerja lainnya. Saat ini, banyak perbaikan dalam .NET telah diimplementasikan dalam hal konstruksi bahasa tambahan menggunakan asinkron, API dengan dukungan asinkron telah diimplementasikan, perbaikan mendasar telah dibuat dalam infrastruktur karena async / await berfungsi seperti jam (khususnya, kinerja dan kemampuan diagnostik telah ditingkatkan dalam. NET Core).

ConfigureAwait adalah salah satu aspek async / await yang terus menimbulkan pertanyaan. Saya harap saya bisa menjawab banyak dari mereka. Saya akan mencoba membuat artikel ini dapat dibaca dari awal hingga akhir, dan pada saat yang sama mengeksekusi dengan gaya jawaban untuk pertanyaan yang sering diajukan (FAQ) sehingga dapat direferensikan di masa depan.

Untuk benar-benar berurusan dengan ConfigureAwait , kami akan kembali sedikit.

Apa itu SynchronizationContext?


Menurut dokumentasi System.Threading.SynchronizationContext "Menyediakan fungsionalitas dasar untuk mendistribusikan konteks sinkronisasi dalam berbagai model sinkronisasi." Definisi ini tidak sepenuhnya jelas.

Dalam 99,9% kasus, SynchronizationContext digunakan hanya sebagai jenis dengan metode virtual Post , yang menerima delegasi untuk eksekusi asynchronous (ada anggota virtual lainnya di SynchronizationContext , tetapi mereka kurang umum dan tidak akan dibahas dalam artikel ini). Metode Post dari tipe dasar secara harfiah hanya memanggil ThreadPool.QueueUserWorkItem untuk secara asinkron mengeksekusi delegasi yang disediakan. Tipe turunan menimpa Post sehingga delegasi dapat mengeksekusi di tempat yang tepat pada waktu yang tepat.

Misalnya, Windows Forms memiliki tipe yang diturunkan dari SynchronizationContext yang mendefinisikan kembali Post untuk membuat yang setara dengan Control.BeginInvoke . Ini berarti bahwa setiap panggilan ke metode Post ini akan menghasilkan panggilan ke delegasi pada tahap selanjutnya di utas yang terkait dengan Kontrol yang sesuai - yang disebut utas UI. Di jantung Windows Forms adalah pemrosesan pesan Win32. Loop pesan dieksekusi di utas UI yang hanya menunggu pesan baru untuk diproses. Pesan-pesan ini dipicu oleh gerakan mouse, klik, input keyboard, peristiwa sistem yang tersedia untuk dieksekusi oleh delegasi, dll. Oleh karena itu, jika Anda memiliki instance SynchronizationContext untuk utas UI di aplikasi Windows Forms, Anda harus meneruskan delegasi ke metode Post untuk melakukan operasi di dalamnya.

Windows Presentation Foundation (WPF) juga memiliki tipe yang diturunkan dari SynchronizationContext dengan metode Post diganti yang juga “mengarahkan” delegasi ke aliran UI (menggunakan Dispatcher.BeginInvoke ), dengan kontrol WPF Dispatcher, bukan kontrol Windows Forms.

Dan Windows RunTime (WinRT) memiliki jenis turunan SynchronizationContext -nya sendiri, yang juga menempatkan delegasi dalam CoreDispatcher utas UI menggunakan CoreDispatcher .

Inilah yang ada di balik frasa “run delegate in UI thread”. Anda juga dapat mengimplementasikan SynchronizationContext Anda dengan metode Post dan beberapa implementasi. Misalnya, saya tidak perlu khawatir tentang utas mana yang dijalankan oleh delegasi, tetapi saya ingin memastikan bahwa setiap delegasi metode Post di SynchronizationContext saya berjalan dengan beberapa derajat paralelisme yang terbatas. Anda dapat menerapkan Context SynchronizationContext kustom dengan cara ini:

 internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext { private readonly SemaphoreSlim _semaphore; public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) => _semaphore = new SemaphoreSlim(maxConcurrencyLevel); public override void Post(SendOrPostCallback d, object state) => _semaphore.WaitAsync().ContinueWith(delegate { try { d(state); } finally { _semaphore.Release(); } }, default, TaskContinuationOptions.None, TaskScheduler.Default); public override void Send(SendOrPostCallback d, object state) { _semaphore.Wait(); try { d(state); } finally { _semaphore.Release(); } } } 

Kerangka kerja xUnit memiliki implementasi yang sama dari SynchronizationContext. Di sini digunakan untuk mengurangi jumlah kode yang terkait dengan tes paralel.

Keuntungan di sini adalah sama dengan abstraksi apa pun: API tunggal disediakan yang dapat digunakan untuk mengantri delegasi untuk dieksekusi dengan cara yang diinginkan programmer, tanpa harus mengetahui detail implementasi. Misalkan saya menulis perpustakaan di mana saya perlu melakukan beberapa pekerjaan dan kemudian mengantri delegasi kembali ke konteks aslinya. Untuk melakukan ini, saya perlu menangkap SynchronizationContext -nya, dan ketika saya menyelesaikan apa yang diperlukan, saya hanya perlu memanggil metode Post dari konteks ini dan mengirimkannya delegasi untuk dieksekusi. Saya tidak perlu tahu bahwa untuk Formulir Windows Anda harus mengambil Control dan menggunakan BeginInvoke -nya, untuk WPF menggunakan BeginInvoke dari Dispatcher , atau entah bagaimana mendapatkan konteks dan antriannya untuk xUnit. Yang perlu saya lakukan adalah mengambil SynchronizationContext saat ini dan menggunakannya nanti. Untuk melakukan ini, SynchronizationContext memiliki properti Current . Ini dapat diimplementasikan sebagai berikut:

 public void DoWork(Action worker, Action completion) { SynchronizationContext sc = SynchronizationContext.Current; ThreadPool.QueueUserWorkItem(_ => { try { worker(); } finally { sc.Post(_ => completion(), null); } }); } 

Anda dapat mengatur konteks khusus dari properti Current menggunakan metode SynchronizationContext.SetSynchronizationContext .

Apa itu Penjadwal Tugas?


SynchronizationContext adalah abstraksi umum untuk "scheduler". Beberapa kerangka kerja menggunakan abstraksi mereka sendiri untuk itu, dan System.Threading.Tasks tidak terkecuali. Ketika ada delegasi di Task yang dapat antri dan dieksekusi, mereka terkait dengan System.Threading.Tasks.TaskScheduler . Ada juga metode Post virtual untuk mengantri delegasi (panggilan delegasi diimplementasikan menggunakan mekanisme standar), TaskScheduler menyediakan metode QueueTask abstrak (panggilan tugas diimplementasikan menggunakan metode ExecuteTask ).

Penjadwal default yang mengembalikan TaskScheduler.Default adalah kumpulan utas. Dari TaskScheduler juga dimungkinkan untuk mendapatkan dan mengganti metode untuk mengatur waktu dan tempat pemanggilan Task . Misalnya, pustaka inti menyertakan tipe System.Threading.Tasks.ConcurrentExclusiveSchedulerPair . Sebuah instance dari kelas ini menyediakan dua properti TaskScheduler : ExclusiveScheduler dan ConcurrentScheduler . Tugas-tugas yang dijadwalkan di ConcurrentScheduler dapat dilakukan secara paralel, tetapi dengan mempertimbangkan batasan yang ditetapkan oleh ConcurrentExclusiveSchedulerPair ketika dibuat (mirip dengan MaxConcurrencySynchronizationContext ). Tidak ada tugas ConcurrentScheduler akan dieksekusi jika tugas dieksekusi dalam ExclusiveScheduler dan hanya satu tugas eksklusif yang diizinkan untuk dijalankan pada suatu waktu. Perilaku ini sangat mirip dengan kunci baca / tulis.

Seperti SynchronizationContext , TaskScheduler memiliki properti Current yang mengembalikan TaskScheduler saat ini. Namun, tidak seperti SynchronizationContext , ia tidak memiliki metode untuk mengatur penjadwal saat ini. Sebagai gantinya, penjadwal dikaitkan dengan tugas saat ini. Jadi, misalnya, program ini akan menampilkan True , karena lambda yang digunakan di StartNew dieksekusi dalam instance ExclusiveScheduler dari ConcurrentExclusiveSchedulerPair , dan TaskScheduler.Current diinstal pada penjadwal ini:

 using System; using System.Threading.Tasks; class Program { static void Main() { var cesp = new ConcurrentExclusiveSchedulerPair(); Task.Factory.StartNew(() => { Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait(); } } 

Menariknya, TaskScheduler menyediakan metode FromCurrentSynchronizationContext statis. Metode ini membuat TaskScheduler baru dan itu TaskScheduler tugas untuk dieksekusi dalam konteks SynchronizationContext.Current kembali menggunakan metode Post .

Bagaimana SynchronizationContext dan TaskScheduler terkait dengan menunggu?


Katakanlah Anda perlu menulis aplikasi UI dengan tombol. Menekan tombol akan memulai pengunduhan teks dari situs web dan menetapkannya ke tombol Content . Tombol harus dapat diakses hanya dari UI aliran di mana ia berada, oleh karena itu, ketika kami berhasil memuat tanggal dan waktu dan ingin menempatkannya di Content tombol, kami perlu melakukan ini dari aliran yang memiliki kendali atasnya. Jika kondisi ini tidak terpenuhi, kami akan mendapatkan pengecualian:

 System.InvalidOperationException: '        ,     .' 

Kami dapat secara manual menggunakan SynchronizationContext untuk mengatur Content dalam konteks sumber, misalnya melalui TaskScheduler :

 private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { downloadBtn.Content = downloadTask.Result; }, TaskScheduler.FromCurrentSynchronizationContext()); } 

Dan kita dapat menggunakan SynchronizationContext secara langsung:

 private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { SynchronizationContext sc = SynchronizationContext.Current; s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { sc.Post(delegate { downloadBtn.Content = downloadTask.Result; }, null); }); } 

Namun, kedua opsi ini secara eksplisit menggunakan callback. Sebagai gantinya, kita dapat menggunakan async / await :

 private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; } 

Semua ini "hanya berfungsi" dan berhasil mengkonfigurasi Content di utas UI, karena dalam kasus versi yang diterapkan secara manual di atas, secara default, menunggu tugas mengacu pada SynchronizationContext.Current dan TaskScheduler.Current . Ketika Anda "mengharapkan" sesuatu dalam C #, kompiler mengubah kode untuk polling (dengan memanggil GetAwaiter ) "diharapkan" (dalam hal ini, Tugas) menjadi "menunggu" ( TaskAwaiter ). "Menunggu" bertanggung jawab untuk melampirkan panggilan balik (sering disebut "kelanjutan") yang memanggil kembali ke mesin negara ketika menunggu selesai. Dia mengimplementasikan ini menggunakan konteks / penjadwal yang dia tangkap selama pendaftaran panggilan balik. Kami akan mengoptimalkan dan mengonfigurasi sedikit, ini seperti ini:

 object scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; } 

Di sini, pertama kali diperiksa apakah SynchronizationContext , dan jika tidak, apakah TaskScheduler standar. Jika ada, maka ketika callback siap untuk panggilan, scheduler yang ditangkap akan digunakan; jika tidak, panggilan balik akan dieksekusi sebagai bagian dari operasi yang menyelesaikan tugas yang diharapkan.

Apa yang dilakukan ConfigureAwait (false)


Metode ConfigureAwait tidak spesial: tidak dikenali dengan cara tertentu oleh kompiler atau runtime. Ini adalah metode normal yang mengembalikan struktur ( ConfiguredTaskAwaitable - membungkus tugas asli) dan mengambil nilai Boolean. Ingat bahwa await dapat digunakan dengan jenis apa pun yang menerapkan pola yang benar. Jika tipe lain dikembalikan, itu berarti ketika kompiler mendapatkan akses ke metode GetAwaiter (bagian dari pola) dari instance, tetapi apakah itu dari tipe yang dikembalikan dari ConfigureAwait , dan bukan dari tugas secara langsung. Ini memungkinkan Anda untuk mengubah perilaku await untuk penunggu khusus ini.

Menunggu tipe yang dikembalikan oleh ConfigureAwait(continueOnCapturedContext: false) alih-alih menunggu Task secara langsung memengaruhi implementasi penangkapan konteks / penjadwal yang dibahas di atas. Logikanya menjadi seperti ini:

 object scheduler = null; if (continueOnCapturedContext) { scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; } } 

Dengan kata lain, menentukan false , bahkan jika ada konteks saat ini atau penjadwal untuk panggilan balik, menyiratkan bahwa itu tidak ada.

Mengapa saya perlu menggunakan ConfigureAwait (false)?


ConfigureAwait(continueOnCapturedContext: false) digunakan untuk mencegah panggilan balik dipaksa memanggil dalam konteks sumber atau penjadwal. Ini memberi kita beberapa keuntungan:

Peningkatan kinerja. Ada overhead antrian panggilan balik, tidak seperti hanya menelepon, karena ini membutuhkan pekerjaan tambahan (dan biasanya alokasi tambahan). Selain itu, kami tidak dapat menggunakan optimasi saat runtime (kami dapat mengoptimalkan lebih banyak ketika kami tahu persis bagaimana panggilan balik akan dipanggil, tetapi jika itu diteruskan ke implementasi abstraksi yang sewenang-wenang, kadang-kadang ini memberlakukan batasan). Untuk bagian yang sarat muatan, bahkan biaya tambahan untuk memeriksa SynchronizationContext saat ini dan TaskScheduler saat ini (keduanya menyiratkan akses ke aliran statis) dapat secara signifikan meningkatkan overhead. Jika kode setelah await tidak memerlukan eksekusi dalam konteks asli, menggunakan ConfigureAwait(false) , semua biaya ini dapat dihindari, karena tidak perlu diantrekan secara tidak perlu, dapat menggunakan semua optimasi yang tersedia, dan juga dapat menghindari akses yang tidak perlu ke statika aliran.

Pencegahan kebuntuan. Pertimbangkan metode pustaka yang await digunakan untuk mengunduh sesuatu dari jaringan. Anda memanggil metode ini dan secara sinkron memblokir, menunggu Tugas selesai, misalnya, menggunakan .Wait() atau .Result atau .GetAwaiter() .GetResult() . Sekarang pertimbangkan apa yang terjadi jika panggilan terjadi ketika SynchronizationContext saat ini membatasi jumlah operasi di dalamnya untuk 1 secara eksplisit menggunakan MaxConcurrencySynchronizationContext , atau secara implisit, jika itu adalah konteks dengan utas tunggal untuk digunakan (misalnya, utas UI). Dengan demikian, Anda memanggil metode dalam utas tunggal, dan kemudian memblokirnya, menunggu operasi selesai. Pengunduhan dimulai melalui jaringan dan menunggu penyelesaiannya. Secara default, menunggu Task menangkap SynchronizationContext saat ini (dan dalam hal ini), dan ketika unduhan dari jaringan selesai, itu akan diantrikan kembali ke callback SynchronizationContext , yang akan memanggil sisa operasi. Tetapi satu-satunya utas yang dapat menangani panggilan balik dalam antrian saat ini diblokir sambil menunggu operasi selesai. Dan operasi ini tidak akan selesai sampai callback diproses. Kebuntuan! Itu dapat terjadi bahkan ketika konteksnya tidak membatasi konkurensi ke 1, tetapi sumber daya terbatas dalam beberapa cara. Bayangkan situasi yang sama, hanya dengan nilai 4 untuk MaxConcurrencySynchronizationContext . Alih-alih mengeksekusi operasi sekali, kami mengantri 4 panggilan ke konteks. Setiap panggilan dibuat dan terkunci untuk mengantisipasi penyelesaiannya. Semua sumber daya sekarang diblokir menunggu penyelesaian metode asinkron, dan satu-satunya hal yang akan memungkinkan mereka untuk menyelesaikan adalah jika panggilan balik mereka diproses oleh konteks ini. Namun, dia sudah sepenuhnya sibuk. Jalan buntu lagi. Jika metode pustaka menggunakan ConfigureAwait(false) sebagai gantinya, itu tidak akan mengantri panggilan balik ke konteks asli, yang akan menghindari skrip kebuntuan.

Apakah saya perlu menggunakan ConfigureAwait (true)?


Tidak, kecuali Anda perlu secara eksplisit menunjukkan bahwa Anda tidak menggunakan ConfigureAwait(false) (misalnya, untuk menyembunyikan peringatan analisis statis, dll.). ConfigureAwait(true) tidak melakukan apa pun yang signifikan. Jika Anda membandingkan await task dan await task await task.ConfigureAwait(true) , mereka akan identik secara fungsional. Dengan demikian, jika ConfigureAwait(true) ada dalam kode, itu dapat dihapus tanpa konsekuensi negatif.

Metode ConfigureAwait mengambil nilai boolean, karena dalam beberapa situasi mungkin perlu melewati variabel untuk mengontrol konfigurasi. Namun dalam 99% kasus, nilai ditetapkan ke false, ConfigureAwait(false) .

Kapan menggunakan ConfigureAwait (false)?


Itu tergantung pada apakah Anda menerapkan kode tingkat aplikasi atau kode perpustakaan tujuan umum.

Saat menulis aplikasi, beberapa perilaku default biasanya diperlukan. Jika model / lingkungan aplikasi (misalnya, Windows Forms, WPF, ASP.NET Core) menerbitkan Konteks SynchronizationContext khusus, hampir pasti ada alasan bagus untuk ini: ini berarti bahwa kode memungkinkan Anda untuk menjaga konteks sinkronisasi untuk interaksi yang tepat dengan model / lingkungan aplikasi. Misalnya, jika Anda menulis event handler di aplikasi Windows Forms, tes di xUnit, atau kode di pengontrol ASP.NET MVC, terlepas dari apakah model aplikasi telah menerbitkan SynchronizationContext , Anda perlu menggunakan SynchronizationContext jika ada. Ini berarti jika ConfigureAwait(true) dan await , panggilan balik / lanjutan dikirim kembali ke konteks asli - semuanya berjalan sebagaimana mestinya. Dari sini Anda dapat merumuskan aturan umum: jika Anda menulis kode tingkat aplikasi, jangan gunakan ConfigureAwait(false) . Mari kita kembali ke penangan klik:

 private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; } 

downloadBtn.Content = text harus dijalankan dalam konteks asli. Jika kode melanggar aturan ini dan menggunakan ConfigureAwait (false) , maka itu tidak akan digunakan dalam konteks asli:

 private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false); //  downloadBtn.Content = text; } 

ini akan mengarah pada perilaku yang tidak pantas. Hal yang sama berlaku untuk kode dalam aplikasi ASP.NET klasik yang bergantung pada HttpContext.Current . Saat menggunakan ConfigureAwait(false) upaya berikutnya untuk menggunakan fungsi Context.Current cenderung menyebabkan masalah.

Inilah yang membedakan perpustakaan tujuan umum. Mereka universal sebagian karena mereka tidak peduli dengan lingkungan di mana mereka digunakan. Anda dapat menggunakannya dari aplikasi web, dari aplikasi klien atau dari tes - tidak masalah, karena kode pustaka adalah agnostik untuk model aplikasi yang dapat digunakan. Agnostik juga berarti bahwa perpustakaan tidak akan melakukan apa pun untuk berinteraksi dengan model aplikasi, misalnya, itu tidak akan mendapatkan akses ke kontrol antarmuka pengguna, karena perpustakaan tujuan umum tidak tahu apa-apa tentang mereka. Karena tidak perlu menjalankan kode di lingkungan tertentu, kami dapat menghindari memaksa kelanjutan / panggilan balik untuk dipaksa ke konteks asli, dan kami melakukan ini menggunakan ConfigureAwait(false) , yang memberi kami keuntungan kinerja dan meningkatkan keandalan. Ini membawa kita pada yang berikut: jika Anda menulis kode perpustakaan untuk keperluan umum, gunakan ConfigureAwait(false) . Inilah sebabnya mengapa setiap (atau hampir setiap) menunggu di pustaka runtime .NET Core menggunakan ConfigureAwait (false); Dengan beberapa pengecualian, yang kemungkinan besar adalah bug, mereka akan diperbaiki.Misalnya, PR ini memperbaiki panggilan yang tidak ConfigureAwait(false)masuk HttpClient.

Tentu saja, ini tidak masuk akal di mana-mana. Misalnya, salah satu pengecualian besar (atau setidaknya kasus di mana Anda perlu memikirkannya) di perpustakaan tujuan umum adalah ketika perpustakaan ini memiliki API yang menerima delegasi untuk panggilan. Dalam kasus seperti itu, perpustakaan menerima kode tingkat aplikasi potensial dari pemanggil, yang membuat asumsi untuk perpustakaan tujuan umum sangat kontroversial. Bayangkan, misalnya, versi asinkron metode Where LINQ: public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate)Haruskah itu predicatedipanggil dalam sumber SynchronizationContextkode panggilan? Itu tergantung pada implementasi WhereAsync, dan inilah alasan mengapa ia mungkin memutuskan untuk tidak menggunakan ConfigureAwait(false).

Bahkan dalam kasus khusus, ikuti rekomendasi umum: gunakan ConfigureAwait(false)jika Anda menulis perpustakaan tujuan umum / aplikasi-model-kode agnostik.

Apakah ConfigureAwait (false) menjamin bahwa callback tidak akan dieksekusi dalam konteks asli?


Tidak, ini memastikan bahwa itu tidak akan masuk ke konteks asli. Tetapi ini tidak berarti bahwa kode setelahnya awaittidak akan dieksekusi dalam konteks aslinya. Ini disebabkan oleh fakta bahwa operasi yang telah selesai dikembalikan secara serempak, dan tidak secara paksa dikembalikan ke antrian. Karenanya, jika Anda mengharapkan tugas yang telah selesai pada saat Anda menunggu, terlepas dari apakah Anda menggunakannya ConfigureAwait(false), kode segera setelah itu akan terus dieksekusi di utas saat ini dalam konteks yang masih valid.

ConfigureAwait (false) , — ?


Secara umum, tidak. Ingat FAQ sebelumnya. Jika itu await task.ConfigureAwait(false)termasuk tugas yang telah selesai pada saat menunggu (yang sebenarnya cukup sering terjadi), maka penggunaan ConfigureAwait(false)akan menjadi tidak berarti, karena utas terus mengeksekusi kode berikut dalam metode dan masih dalam konteks yang sama seperti sebelumnya.

Satu pengecualian penting adalah bahwa yang pertama awaitakan selalu berakhir secara tidak sinkron, dan operasi yang diharapkan akan memanggilnya kembali di lingkungan yang bebas dari SynchronizationContextatau khusus TaskScheduler. Misalnya, CryptoStreamdi pustaka runtime, .NET memverifikasi bahwa kode berpotensi intensif komputasi tidak dieksekusi sebagai bagian dari permintaan kode panggilan pemanggilan yang sinkron. Untuk melakukan ini, ia menggunakan khususawaiteruntuk memastikan bahwa kode setelah menunggu pertama dijalankan di utas thread pool. Namun, bahkan dalam kasus ini, Anda akan melihat bahwa penantian selanjutnya masih menggunakan ConfigureAwait(false); Secara teknis, ini tidak perlu, tetapi sangat menyederhanakan tinjauan kode, karena tidak perlu memahami mengapa itu tidak digunakan ConfigureAwait(false).

Apakah mungkin menggunakan Task.Run untuk menghindari penggunaan ConfigureAwait (false)?


Ya, jika Anda menulis:

 Task.Run(async delegate { await SomethingAsync(); //     }); 

ConfigureAwait(false) SomethingAsync() , , Task.Run , , SynchronizationContext.Current null . , Task.Run TaskScheduler.Default , TaskScheduler.Current Default . , await , ConfigureAwait(false) . , . :

 Task.Run(async delegate { SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx()); await SomethingAsync(); //    SomeCoolSyncCtx }); 

maka kode di dalamnya SomethingAsyncakan benar-benar melihat SynchronizationContext.Currentinstance SomeCoolSyncCtx. dan ini await, dan setiap harapan yang tidak dikonfigurasi di dalam SomethingAsync akan dikembalikan ke konteks ini. Jadi, untuk menggunakan pendekatan ini, perlu dipahami apa yang dapat dilakukan atau tidak dilakukan oleh semua kode yang Anda masukkan dalam antrian, dan apakah tindakannya dapat menjadi penghalang.

Pendekatan ini juga terjadi karena kebutuhan untuk membuat / mengantri objek tugas tambahan. Ini mungkin atau mungkin tidak penting bagi aplikasi / perpustakaan, tergantung pada persyaratan kinerja.

Juga perlu diingat bahwa solusi semacam itu dapat menyebabkan lebih banyak masalah daripada manfaat dan memiliki konsekuensi yang tidak diinginkan yang berbeda. Misalnya, beberapa alat analisis statis menandai harapan yang tidak menggunakan ConfigureAwait(false) CA2007 . Jika Anda menghidupkan penganalisis, dan kemudian menggunakan trik semacam itu untuk menghindari penggunaan ConfigureAwait, ada kemungkinan besar bahwa penganalisa akan menandainya. Ini mungkin memerlukan lebih banyak pekerjaan, misalnya, Anda mungkin ingin menonaktifkan analyzer karena sifatnya yang penting, dan ini akan memerlukan melewatkan tempat-tempat lain dalam basis kode di mana Anda benar-benar perlu menggunakannya ConfigureAwait(false).

Apakah mungkin menggunakan SynchronizationContext.SetSynchronizationContext untuk menghindari penggunaan ConfigureAwait (false)?


Tidak.Meskipun itu mungkin. Itu tergantung pada implementasi yang digunakan.

Beberapa pengembang melakukan ini:

 Task t; SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { t = CallCodeThatUsesAwaitAsync(); // await'      } finally { SynchronizationContext.SetSynchronizationContext(old); } await t; //  -     


dengan harapan ini akan memaksa kode di dalam untuk CallCodeThatUsesAwaitAsyncmelihat konteks saat ini sebagai null. Memang akan begitu. Namun, opsi ini tidak akan memengaruhi yang awaitdilihatnya TaskScheduler.Current. Oleh karena itu, jika kode dieksekusi dalam spesial TaskScheduler, awaitdi dalamnya CallCodeThatUsesAwaitAsyncakan melihat dan mengantri untuk spesial itu TaskScheduler.

Seperti dalam Task.RunFAQ, peringatan yang sama berlaku di sini: ada konsekuensi tertentu dari pendekatan ini, dan kode di dalam blok tryjuga dapat mengganggu upaya ini dengan menetapkan konteks yang berbeda (atau memanggil kode menggunakan penjadwal tugas yang tidak standar).

Dengan templat ini, Anda juga perlu berhati-hati dengan perubahan kecil:

 SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { await t; } finally { SynchronizationContext.SetSynchronizationContext(old); } 

Lihat apa masalahnya? Agak sulit untuk diperhatikan, tetapi itu mengesankan. Tidak ada jaminan bahwa menunggu pada akhirnya akan menyebabkan panggilan balik / melanjutkan di utas asli. Ini berarti bahwa pengembalian SynchronizationContextke sumber asli mungkin tidak terjadi di utas asli, yang dapat mengarah pada fakta bahwa item kerja berikutnya di utas ini akan melihat konteks yang salah. Untuk mengatasi hal ini, model aplikasi yang ditulis dengan baik yang menetapkan konteks khusus biasanya menambahkan kode untuk meresetnya secara manual sebelum memanggil kode kustom tambahan. Dan bahkan jika ini terjadi dalam satu utas, mungkin butuh beberapa waktu di mana konteksnya mungkin tidak dipulihkan dengan benar. Dan jika itu bekerja di utas yang berbeda, ini dapat menyebabkan pemasangan konteks yang salah. Dan sebagainya. Cukup jauh dari ideal.

Apakah saya perlu menggunakan ConfigureAwait (false) jika saya menggunakan GetAwaiter () .GetResult ()?


Tidak. ConfigureAwaithanya memengaruhi panggilan balik. Secara khusus, templat awaitermengharuskan Anda awaitermemberikan properti IsCompleted, metode, GetResultdan OnCompleted(opsional dengan metode UnsafeOnCompleted). ConfigureAwaithanya mempengaruhi perilaku {Unsafe}OnCompleted, jadi jika Anda langsung menelepon GetResult(), terlepas dari apakah Anda melakukannya TaskAwaiteratau tidak ConfiguredTaskAwaitable.ConfiguredTaskAwaiterada perbedaan dalam perilaku. Karena itu, jika Anda melihat task.ConfigureAwait(false).GetAwaiter().GetResult()Anda dapat menggantinya dengan task.GetAwaiter().GetResult()(selain itu, pikirkan apakah Anda benar-benar membutuhkan implementasi seperti itu).

Saya tahu bahwa kode berjalan di lingkungan di mana tidak akan pernah ada SynchronizationContext khusus atau TaskScheduler khusus. Bisakah saya tidak menggunakan ConfigureAwait (false)?


MungkinItu tergantung pada seberapa yakin Anda tentang "tidak pernah." Seperti disebutkan dalam pertanyaan sebelumnya, hanya karena model aplikasi yang sedang Anda kerjakan tidak menentukan yang khusus SynchronizationContextdan tidak memanggil kode Anda dalam yang khusus TaskSchedulertidak berarti bahwa kode dari pengguna atau pustaka lain tidak menggunakannya. Jadi, Anda harus yakin akan hal ini, atau setidaknya mengakui risiko bahwa opsi semacam itu mungkin.

Saya mendengar bahwa di .NET Core tidak perlu menerapkan ConfigureAwait (false). Benarkah begitu?


Tidak seperti itu.Diperlukan saat bekerja di .NET Core untuk alasan yang sama seperti ketika bekerja di .NET Framework. Tidak ada yang berubah dalam hal ini.

Itu telah mengubah apakah lingkungan tertentu mempublikasikan lingkungan mereka SynchronizationContext. Secara khusus, sementara ASP.NET klasik di .NET Framework memiliki sendiri SynchronizationContext, ASP.NET Core tidak. Ini berarti bahwa kode yang berjalan di aplikasi Core ASP.NET tidak akan melihat kode khusus secara default SynchronizationContext, yang mengurangi kebutuhan ConfigureAwait(false)untuk lingkungan ini.

Namun, ini tidak berarti bahwa tidak akan pernah ada kebiasaan SynchronizationContextatauTaskScheduler. Jika ada kode pengguna (atau kode perpustakaan lain yang digunakan oleh aplikasi) menetapkan konteks pengguna dan memanggil kode Anda atau memanggil kode Anda dalam Tugas yang dijadwalkan dalam penjadwal tugas khusus, maka awaitCore ASP.NET akan melihat konteks atau penjadwal non-standar, yang mungkin mengharuskan penggunaan ConfigureAwait(false). Tentu saja, dalam situasi di mana Anda menghindari kunci sinkron (yang perlu Anda lakukan dalam aplikasi web) dan jika Anda tidak menentang overhead kinerja kecil dalam beberapa kasus, Anda dapat melakukannya tanpa menggunakan ConfigureAwait(false).

Bisakah saya menggunakan ConfigureAwait ketika "menunggu foreach selesai" pada IAsyncEnumerable?


YaLihat artikel MSDN sebagai contoh .

Await foreachIni sesuai dengan pola dan dengan demikian dapat digunakan untuk daftar IAsyncEnumerable<T>. Itu juga dapat digunakan untuk membuat daftar elemen yang mewakili cakupan API yang benar. NET runtime perpustakaan termasuk metode ekspansi ConfigureAwait untuk IAsyncEnumerable<T>yang mengembalikan tipe khusus, yang membungkus IAsyncEnumerable<T>dan Booleanberkorespondensi ke template yang benar. Ketika kompiler menghasilkan panggilan ke MoveNextAsyncdan DisposeAsyncenumerator. Panggilan-panggilan ini terkait dengan jenis struktur enumerator yang dikonfigurasi yang dikembalikan, yang pada gilirannya memenuhi harapan sebagaimana diperlukan.

Bisakah saya menggunakan ConfigureAwait dengan 'menunggu menggunakan' IAsyncDisposable?


Ya, meski dengan sedikit kerumitan.

Seperti halnya IAsyncEnumerable<T>, perpustakaan .NET runtime menyediakan metode ekstensi ConfigureAwaituntuk IAsyncDisposabledan await usingakan berfungsi dengan baik karena mengimplementasikan template yang sesuai (yaitu, ia menyediakan metode yang sesuai DisposeAsync):

 await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... } 

Masalahnya di sini adalah bahwa jenisnya csekarang bukan MyAsyncDisposableClass, melainkan System.Runtime.CompilerServices.ConfiguredAsyncDisposable, yang dikembalikan dari metode ekstensi ConfigureAwaituntuk IAsyncDisposable.

Untuk menyiasatinya, tambahkan baris:

 var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... } 

Sekarang jenis yang cdiinginkan lagi MyAsyncDisposableClass. Yang juga memiliki efek meningkatkan ruang lingkup untuk c; jika perlu, Anda bisa membungkusnya dengan kawat gigi.

Saya menggunakan ConfigureAwait (false), tetapi AsyncLocal saya masih mengalir ke kode setelah menunggu. Apakah ini bug?


Tidak, ini cukup diharapkan. Aliran data AsyncLocal<T>adalah bagian ExecutionContextyang terpisah dari SynchronizationContext. Kecuali jika Anda secara eksplisit menonaktifkan aliran ExecutionContextmenggunakan ExecutionContext.SuppressFlow(), ExecutionContext(dan dengan demikian data AsyncLocal <T>) akan selalu melalui awaits, terlepas dari apakah itu digunakan untuk ConfigureAwaitmenghindari pengambilan yang asli SynchronizationContext. Rincian lebih lanjut dibahas dalam artikel ini .

Bisakah alat bahasa membantu saya menghindari kebutuhan untuk secara eksplisit menggunakan ConfigureAwait (false) di perpustakaan saya?


Pengembang perpustakaan terkadang mengeluh tentang perlunya menggunakan ConfigureAwait(false)dan meminta alternatif yang kurang invasif.

Saat ini mereka tidak, setidaknya mereka tidak dibangun ke dalam bahasa / kompiler / runtime. Namun, ada banyak saran tentang bagaimana ini dapat diimplementasikan, misalnya: 1 , 2 , 3 , 4 .

Jika topik yang Anda minati, jika Anda memiliki ide-ide baru dan menarik, penulis artikel asli mengundang Anda untuk berdiskusi.

Source: https://habr.com/ru/post/id482354/


All Articles