Praktik terbaik untuk memanggil ConfigureAwait untuk semua kode sisi server

561

Ketika Anda memiliki kode sisi server (yaitu beberapa ApiController) dan fungsi Anda tidak sinkron - sehingga kembali Task<SomeObject>- apakah dianggap praktik terbaik yang setiap kali Anda menunggu fungsi yang Anda panggil ConfigureAwait(false)?

Saya telah membaca bahwa itu lebih berkinerja karena tidak harus mengubah konteks utas kembali ke konteks utas asli. Namun, dengan ASP.NET Web Api, jika permintaan Anda masuk pada satu utas, dan Anda menunggu beberapa fungsi dan panggilan ConfigureAwait(false)yang berpotensi menempatkan Anda pada utas yang berbeda ketika Anda mengembalikan hasil akhir dari ApiControllerfungsi Anda .

Saya telah mengetik contoh dari apa yang saya bicarakan di bawah ini:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}
Arash Emami
sumber

Jawaban:

628

Pembaruan: ASP.NET Core tidak memilikiSynchronizationContext . Jika Anda menggunakan ASP.NET Core, tidak masalah apakah Anda menggunakan ConfigureAwait(false)atau tidak.

Untuk ASP.NET "Penuh" atau "Klasik" atau apa pun, sisa jawaban ini masih berlaku.

Posting asli (untuk ASP.NET non-Core):

Video ini oleh tim ASP.NET memiliki informasi terbaik tentang penggunaan asyncdi ASP.NET.

Saya telah membaca bahwa itu lebih berkinerja karena tidak harus mengubah konteks utas kembali ke konteks utas asli.

Ini berlaku untuk aplikasi UI, di mana hanya ada satu utas UI yang harus "disinkronkan" kembali.

Di ASP.NET, situasinya sedikit lebih kompleks. Ketika suatu asyncmetode melanjutkan eksekusi, itu mengambil sebuah utas dari kumpulan utas ASP.NET. Jika Anda menonaktifkan pengambilan konteks menggunakan ConfigureAwait(false), maka utas hanya melanjutkan menjalankan metode secara langsung. Jika Anda tidak menonaktifkan tangkapan konteks, maka utas akan memasukkan kembali konteks permintaan dan kemudian melanjutkan untuk mengeksekusi metode.

Jadi ConfigureAwait(false)tidak menghemat lompatan thread di ASP.NET; itu tidak menyelamatkan Anda memasukkan kembali konteks permintaan, tetapi ini biasanya sangat cepat. ConfigureAwait(false) bisa berguna jika Anda mencoba melakukan sejumlah kecil pemrosesan paralel permintaan, tetapi sebenarnya TPL lebih cocok untuk sebagian besar skenario tersebut.

Namun, dengan ASP.NET Web Api, jika permintaan Anda masuk dalam satu utas, dan Anda menunggu beberapa fungsi dan memanggil ConfigureAwait (false) yang berpotensi menempatkan Anda pada utas yang berbeda ketika Anda mengembalikan hasil akhir dari fungsi ApiController Anda .

Sebenarnya, hanya melakukan awaitdapat melakukan itu. Setelah asyncmetode Anda awaitmengenai, metode diblokir tetapi utas kembali ke kumpulan utas. Ketika metode siap untuk melanjutkan, semua utas diambil dari kumpulan utas dan digunakan untuk melanjutkan metode.

Satu-satunya perbedaan ConfigureAwaitdi ASP.NET adalah apakah utas itu memasuki konteks permintaan saat melanjutkan metode.

Saya memiliki lebih banyak informasi latar belakang di artikel MSDNSynchronizationContext saya dan asyncposting blog intro saya .

Stephen Cleary
sumber
23
Penyimpanan thread-local tidak mengalir oleh konteks apa pun . HttpContext.Currentmengalir oleh ASP.NET SynchronizationContext, yang mengalir secara default saat Anda await, tetapi tidak mengalir oleh ContinueWith. OTOH, konteks eksekusi (termasuk pembatasan keamanan) adalah konteks disebutkan dalam CLR melalui C #, dan itu adalah mengalir dengan baik ContinueWithdan await(bahkan jika Anda menggunakan ConfigureAwait(false)).
Stephen Cleary
65
Bukankah lebih bagus jika C # memiliki dukungan bahasa asli untuk ConfigureAwait (false)? Sesuatu seperti 'awaitnc' (tidak menunggu konteks). Mengetik panggilan metode terpisah di mana-mana cukup mengganggu. :)
NathanAldenSr
19
@NathanAldenSr: Itu sudah dibahas sedikit. Masalah dengan kata kunci baru adalah bahwa ConfigureAwaitsebenarnya hanya masuk akal ketika Anda menunggu tugas , sedangkan awaitbertindak pada "ditunggu." Opsi lain yang dipertimbangkan adalah: Haruskah perilaku default membuang konteks jika di perpustakaan? Atau ada pengaturan kompiler untuk perilaku konteks default? Keduanya ditolak karena lebih sulit untuk hanya membaca kode dan mengatakan apa yang dilakukannya.
Stephen Cleary
10
@AnshulNigam: Itulah sebabnya tindakan pengontrol perlu konteksnya. Tetapi sebagian besar metode yang disebut tindakan tidak.
Stephen Cleary
14
@JonathanRoeder: Secara umum, Anda tidak perlu ConfigureAwait(false)untuk menghindari Result/ Waitberbasis kebuntuan karena pada ASP.NET Anda tidak harus menggunakan Result/ Waitdi tempat pertama.
Stephen Cleary
131

Jawaban singkat untuk pertanyaan Anda: Tidak. Anda tidak boleh menelepon ConfigureAwait(false)di tingkat aplikasi seperti itu.

TL; Versi DR dari jawaban yang panjang: Jika Anda menulis perpustakaan di mana Anda tidak mengenal konsumen Anda dan tidak memerlukan konteks sinkronisasi (yang seharusnya tidak Anda temukan di perpustakaan, saya harus selalu menggunakannya ConfigureAwait(false). Jika tidak, konsumen perpustakaan Anda mungkin menghadapi kebuntuan dengan mengonsumsi metode asinkron Anda dengan cara memblokir. Ini tergantung situasi.

Berikut adalah penjelasan yang sedikit lebih rinci tentang pentingnya ConfigureAwaitmetode (kutipan dari posting blog saya):

Ketika Anda sedang menunggu suatu metode dengan kata kunci yang menunggu, kompiler menghasilkan banyak kode untuk Anda. Salah satu tujuan tindakan ini adalah menangani sinkronisasi dengan utas UI (atau utama). Komponen utama dari fitur ini adalah SynchronizationContext.Currentyang mendapatkan konteks sinkronisasi untuk utas saat ini. SynchronizationContext.Currentdiisi sesuai dengan lingkungan tempat Anda berada. GetAwaiterMetode Tugas dicari SynchronizationContext.Current. Jika konteks sinkronisasi saat ini bukan nol, kelanjutan yang diteruskan ke penunggu itu akan dikirim kembali ke konteks sinkronisasi tersebut.

Saat menggunakan metode, yang menggunakan fitur bahasa asinkron baru, dengan cara memblokir, Anda akan berakhir dengan jalan buntu jika Anda memiliki SynchronizationContext yang tersedia. Saat Anda menggunakan metode tersebut dengan cara memblokir (menunggu pada tugas dengan metode Tunggu atau mengambil hasilnya langsung dari properti Hasil Tugas), Anda akan memblokir utas utama pada saat yang sama. Ketika akhirnya Tugas selesai di dalam metode itu di threadpool, itu akan memanggil kelanjutan untuk mengirim kembali ke utas utama karena SynchronizationContext.Currenttersedia dan ditangkap. Tetapi ada masalah di sini: utas UI diblokir dan Anda menemui jalan buntu!

Juga, berikut adalah dua artikel bagus untuk Anda yang tepat untuk pertanyaan Anda:

Akhirnya, ada video pendek yang bagus dari Lucian Wischik tepatnya tentang topik ini: Metode perpustakaan Async harus mempertimbangkan untuk menggunakan Task.ConfigureAwait (false) .

Semoga ini membantu.

tugberk
sumber
2
"Metode tugas GetAwaiter mencari SynchronizationContext.Current. Jika konteks sinkronisasi saat ini bukan nol, kelanjutan yang diteruskan ke penunggu itu akan dikirim kembali ke konteks sinkronisasi." - Saya mendapat kesan bahwa Anda mencoba untuk mengatakan bahwa Taskberjalan tumpukan untuk mendapatkan SynchronizationContext, yang salah. The SynchronizationContextadalah meraih sebelum panggilan ke Taskdan kemudian sisa kode dilanjutkan pada SynchronizationContextjika SynchronizationContext.Currenttidak null.
casperOne
1
@casperOne Saya bermaksud mengatakan hal yang sama.
tugberk
8
Bukankah seharusnya menjadi tanggung jawab penelepon untuk memastikan bahwa SynchronizationContext.Currentitu jelas / atau bahwa perpustakaan dipanggil di dalam Task.Run()daripada harus menulis di .ConfigureAwait(false)seluruh perpustakaan kelas?
binki
1
@binki - di sisi lain: (1) mungkin perpustakaan digunakan di banyak aplikasi, jadi melakukan upaya satu kali di perpustakaan untuk membuatnya lebih mudah pada aplikasi adalah hemat biaya; (2) mungkin penulis perpustakaan tahu bahwa ia telah menulis kode yang tidak memiliki alasan untuk mengharuskan melanjutkan pada konteks asli, yang ia ungkapkan oleh mereka .ConfigureAwait(false). Mungkin akan lebih mudah bagi penulis perpustakaan jika itu adalah perilaku default, tetapi saya akan berasumsi bahwa membuat sedikit lebih sulit untuk menulis perpustakaan dengan benar lebih baik daripada membuatnya sedikit lebih sulit untuk menulis aplikasi dengan benar.
ToolmakerSteve
4
Mengapa penulis perpustakaan memanjakan konsumen? Jika konsumen ingin menemui jalan buntu, mengapa saya harus mencegahnya?
Quarkly
25

Kelemahan terbesar yang saya temukan dengan menggunakan ConfigureAwait (false) adalah budaya thread dikembalikan ke default sistem. Jika Anda mengkonfigurasi budaya misalnya ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

dan Anda hosting di server yang budayanya diatur ke en-AS, maka Anda akan menemukan sebelum ConfigureAwait (false) disebut CultureInfo.CurrentCulture akan mengembalikan en-AU dan setelah Anda mendapatkan en-AS. yaitu

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

Jika aplikasi Anda melakukan sesuatu yang membutuhkan pemformatan khusus data kultur, maka Anda harus memperhatikan hal ini saat menggunakan ConfigureAwait (false).

Mick
sumber
27
Versi modern .NET (saya pikir sejak 4.6?) Akan menyebarkan budaya lintas utas, bahkan jika ConfigureAwait(false)digunakan.
Stephen Cleary
1
Terimakasih atas infonya. Kami memang menggunakan .net 4.5.2
Mick
11

Saya memiliki beberapa pemikiran umum tentang implementasi Task:

  1. Tugas sekali pakai namun kita tidak seharusnya menggunakannya using.
  2. ConfigureAwaitdiperkenalkan pada 4.5. Taskdiperkenalkan pada 4.0.
  3. .NET Threads selalu digunakan untuk mengalir konteks (lihat C # melalui buku CLR) tetapi dalam implementasi default Task.ContinueWithmereka tidak b / c disadari bahwa konteks switch mahal dan dimatikan secara default.
  4. Masalahnya adalah pengembang perpustakaan tidak perlu peduli apakah kliennya memerlukan aliran konteks atau tidak maka tidak harus memutuskan apakah mengalir konteks atau tidak.
  5. [Ditambahkan nanti] Fakta bahwa tidak ada jawaban resmi dan referensi yang tepat dan kami terus memperjuangkan ini berarti seseorang belum melakukan pekerjaannya dengan benar.

Saya sudah mendapat beberapa posting tentang masalah ini tetapi pendapat saya - selain jawaban bagus Tugberk - adalah bahwa Anda harus mengubah semua API tidak sinkron dan idealnya mengalir konteksnya. Karena Anda melakukan async, Anda cukup menggunakan kelanjutan alih-alih menunggu sehingga tidak ada jalan buntu yang disebabkan karena tidak ada menunggu dilakukan di perpustakaan dan Anda tetap mengalir agar konteksnya dipertahankan (seperti HttpContext).

Masalahnya adalah ketika perpustakaan memperlihatkan API sinkron tetapi menggunakan API asinkron lain - maka Anda perlu menggunakan Wait()/ Resultdalam kode Anda.

Aliostad
sumber
6
1) Anda dapat menelepon Task.Disposejika Anda mau; Anda hanya tidak perlu untuk sebagian besar waktu. 2) Taskdiperkenalkan di .NET 4.0 sebagai bagian dari TPL, yang tidak perlu ConfigureAwait; ketika asyncditambahkan, mereka menggunakan kembali Taskjenis yang sudah ada alih-alih menciptakan yang baru Future.
Stephen Cleary
6
3) Anda membingungkan dua jenis "konteks" yang berbeda. "Konteks" yang disebutkan dalam C # via CLR selalu mengalir, bahkan dalam Tasks; "konteks" yang dikendalikan oleh ContinueWithadalah a SynchronizationContextatau TaskScheduler. Konteks yang berbeda ini dijelaskan secara rinci di blog Stephen Toub .
Stephen Cleary
21
4) Penulis perpustakaan tidak perlu peduli apakah peneleponnya memerlukan aliran konteks, karena setiap metode asinkron dilanjutkan kembali secara independen. Jadi jika penelepon membutuhkan aliran konteks, mereka dapat mengalirkannya, terlepas dari apakah penulis perpustakaan mengalirkannya atau tidak.
Stephen Cleary
1
Pada awalnya, Anda tampaknya mengeluh alih-alih menjawab pertanyaan. Dan kemudian Anda berbicara tentang "konteks", kecuali ada beberapa jenis konteks di. Net dan benar-benar tidak jelas yang mana (atau yang?) Yang Anda bicarakan. Dan bahkan jika Anda tidak bingung sendiri (tapi saya pikir Anda bingung, saya percaya tidak ada konteks yang digunakan dengan Threads, tetapi tidak lagi dengan ContinueWith()), ini membuat jawaban Anda membingungkan untuk dibaca.
svick
1
@StephenCleary ya, lib dev seharusnya tidak perlu tahu, itu tergantung klien. Saya pikir saya sudah membuatnya jelas, tetapi frasa saya tidak jelas.
Aliostad