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

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);
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 predicate
dipanggil dalam sumber SynchronizationContext
kode 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 await
tidak 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 await
akan selalu berakhir secara tidak sinkron, dan operasi yang diharapkan akan memanggilnya kembali di lingkungan yang bebas dari SynchronizationContext
atau khusus TaskScheduler
. Misalnya, CryptoStream
di 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 khususawaiter
untuk 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();
maka kode di dalamnya SomethingAsync
akan benar-benar melihat SynchronizationContext.Current
instance 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();
dengan harapan ini akan memaksa kode di dalam untuk CallCodeThatUsesAwaitAsync
melihat konteks saat ini sebagai null
. Memang akan begitu. Namun, opsi ini tidak akan memengaruhi yang await
dilihatnya TaskScheduler.Current
. Oleh karena itu, jika kode dieksekusi dalam spesial TaskScheduler
, await
di dalamnya CallCodeThatUsesAwaitAsync
akan melihat dan mengantri untuk spesial itu TaskScheduler
.Seperti dalam Task.Run
FAQ, peringatan yang sama berlaku di sini: ada konsekuensi tertentu dari pendekatan ini, dan kode di dalam blok try
juga 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 SynchronizationContext
ke 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.
ConfigureAwait
hanya memengaruhi panggilan balik. Secara khusus, templat awaiter
mengharuskan Anda awaiter
memberikan properti IsCompleted
, metode, GetResult
dan OnCompleted
(opsional dengan metode UnsafeOnCompleted). ConfigureAwait
hanya mempengaruhi perilaku {Unsafe}OnCompleted
, jadi jika Anda langsung menelepon GetResult()
, terlepas dari apakah Anda melakukannya TaskAwaiter
atau tidak ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
ada 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)?
Mungkin
Itu 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 SynchronizationContext
dan tidak memanggil kode Anda dalam yang khusus TaskScheduler
tidak 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 SynchronizationContext
atauTaskScheduler
. 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 await
Core 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?
Ya
Lihat artikel MSDN sebagai contoh .Await foreach
Ini 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 Boolean
berkorespondensi ke template yang benar. Ketika kompiler menghasilkan panggilan ke MoveNextAsync
dan DisposeAsync
enumerator. 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 ConfigureAwait
untuk IAsyncDisposable
dan await using
akan 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 c
sekarang bukan MyAsyncDisposableClass
, melainkan System.Runtime.CompilerServices.ConfiguredAsyncDisposable
, yang dikembalikan dari metode ekstensi ConfigureAwait
untuk IAsyncDisposable
.Untuk menyiasatinya, tambahkan baris: var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... }
Sekarang jenis yang c
diinginkan 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 ExecutionContext
yang terpisah dari SynchronizationContext
. Kecuali jika Anda secara eksplisit menonaktifkan aliran ExecutionContext
menggunakan ExecutionContext.SuppressFlow()
, ExecutionContext
(dan dengan demikian data AsyncLocal <T>
) akan selalu melalui awaits
, terlepas dari apakah itu digunakan untuk ConfigureAwait
menghindari 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.