HttpClient.GetAsync (...) tidak pernah kembali saat menggunakan wait / async

315

Sunting: Pertanyaan ini sepertinya masalah yang sama, tetapi tidak memiliki tanggapan ...

Sunting: Dalam uji kasus 5 tugas tampaknya macet dalam WaitingForActivationkeadaan.

Saya telah menemukan beberapa perilaku aneh menggunakan System.Net.Http.HttpClient di .NET 4.5 - di mana "menunggu" hasil panggilan ke (misalnya) httpClient.GetAsync(...)tidak akan pernah kembali.

Ini hanya terjadi dalam keadaan tertentu ketika menggunakan fungsionalitas bahasa async / await baru dan Tasks API - kode tersebut sepertinya selalu berfungsi bila hanya menggunakan kelanjutan.

Berikut ini beberapa kode yang mereproduksi masalah - masukkan ini ke dalam "proyek MVC 4 WebApi" baru di Visual Studio 11 untuk mengekspos titik akhir GET berikut:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

Setiap titik akhir di sini mengembalikan data yang sama (header respons dari stackoverflow.com) kecuali /api/test5yang tidak pernah selesai.

Sudahkah saya menemukan bug di kelas HttpClient, atau apakah saya menyalahgunakan API dengan cara tertentu?

Kode untuk direproduksi:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}
Benjamin Fox
sumber
2
Tampaknya bukan masalah yang sama, tetapi hanya untuk memastikan Anda mengetahuinya, ada bug MVC4 dalam metode async WRT beta yang lengkap secara sinkron - lihat stackoverflow.com/questions/9627329/…
James Manning
Terima kasih - saya akan berhati-hati untuk itu. Dalam hal ini saya berpikir bahwa metode ini harus selalu asinkron karena panggilan ke HttpClient.GetAsync(...)?
Benjamin Fox

Jawaban:

468

Anda menyalahgunakan API.

Inilah situasinya: di ASP.NET, hanya satu utas yang dapat menangani permintaan sekaligus. Anda dapat melakukan beberapa pemrosesan paralel jika perlu (meminjam utas tambahan dari kumpulan utas), tetapi hanya satu utas yang memiliki konteks permintaan (utas tambahan tidak memiliki konteks permintaan).

Ini dikelola oleh ASP.NETSynchronizationContext .

Secara default, ketika Anda awaita Task, metode dilanjutkan pada yang ditangkap SynchronizationContext(atau ditangkap TaskScheduler, jika tidak ada SynchronizationContext). Biasanya, ini hanya yang Anda inginkan: aksi pengontrol asinkron akan menghasilkan awaitsesuatu, dan ketika dilanjutkan, ia melanjutkan dengan konteks permintaan.

Jadi, inilah mengapa test5gagal:

  • Test5Controller.Getdijalankan AsyncAwait_GetSomeDataAsync(dalam konteks permintaan ASP.NET).
  • AsyncAwait_GetSomeDataAsyncdijalankan HttpClient.GetAsync(dalam konteks permintaan ASP.NET).
  • Permintaan HTTP dikirim, dan HttpClient.GetAsyncmengembalikan yang belum selesai Task.
  • AsyncAwait_GetSomeDataAsyncmenunggu Task; karena tidak lengkap, AsyncAwait_GetSomeDataAsyncmengembalikan yang belum selesai Task.
  • Test5Controller.Get memblokir utas saat ini sampai Taskselesai.
  • Respons HTTP masuk, dan Taskdikembalikan oleh HttpClient.GetAsyncselesai.
  • AsyncAwait_GetSomeDataAsyncmencoba untuk melanjutkan dalam konteks permintaan ASP.NET. Namun, sudah ada utas dalam konteks itu: utas diblokir Test5Controller.Get.
  • Jalan buntu.

Inilah mengapa yang lain berfungsi:

  • ( test1,, test2dan test3): Continuations_GetSomeDataAsyncmenjadwalkan kelanjutan ke kumpulan utas, di luar konteks permintaan ASP.NET. Ini memungkinkan pengembalian Taskdengan Continuations_GetSomeDataAsyncmenyelesaikan tanpa harus memasukkan kembali konteks permintaan.
  • ( test4Dan test6): Sejak Taskadalah ditunggu , permintaan ASP.NET benang tidak diblokir. Ini memungkinkan AsyncAwait_GetSomeDataAsyncuntuk menggunakan konteks permintaan ASP.NET ketika siap untuk melanjutkan.

Dan inilah praktik terbaiknya:

  1. Dalam asyncmetode "perpustakaan" Anda , gunakan ConfigureAwait(false)sedapat mungkin. Dalam kasus Anda, ini akan berubah AsyncAwait_GetSomeDataAsyncmenjadivar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Jangan memblokir Tasks; itu asyncsemua jalan turun. Dengan kata lain, gunakan awaitsebagai ganti GetResult( Task.Resultdan Task.Waitjuga harus diganti dengan await).

Dengan begitu, Anda mendapatkan kedua manfaat: kelanjutan (sisa AsyncAwait_GetSomeDataAsyncmetode) dijalankan pada utas utas dasar yang tidak harus memasukkan konteks permintaan ASP.NET; dan controller itu sendiri async(yang tidak memblokir utas permintaan).

Informasi lebih lanjut:

Pembaruan 2012-07-13: Memasukkan jawaban ini ke dalam posting blog .

Stephen Cleary
sumber
2
Apakah ada beberapa dokumentasi untuk ASP.NET SynchroniztaionContextyang menjelaskan bahwa hanya ada satu utas dalam konteks untuk beberapa permintaan? Jika tidak, saya pikir harus ada.
svick
8
Itu tidak didokumentasikan di mana pun AFAIK.
Stephen Cleary
10
Terima kasih - respons yang luar biasa . Perbedaan perilaku antara (tampaknya) kode yang identik secara fungsional membuat frustasi tetapi masuk akal dengan penjelasan Anda. Akan bermanfaat jika kerangka kerja dapat mendeteksi kebuntuan semacam itu dan mengajukan pengecualian di suatu tempat.
Benjamin Fox
3
Apakah ada situasi di mana menggunakan .ConfigureAwait (false) dalam konteks asp.net TIDAK dianjurkan? Tampaknya bagi saya bahwa itu harus selalu digunakan dan itu hanya dalam konteks UI yang seharusnya tidak digunakan karena Anda perlu menyinkronkan ke UI. Atau apakah saya melewatkan intinya?
AlexGad
3
ASP.NET SynchronizationContextmemang menyediakan beberapa fungsi penting: itu mengalir konteks permintaan. Ini termasuk semua jenis barang mulai dari otentikasi hingga cookie hingga kultur. Jadi di ASP.NET, alih-alih menyinkronkan kembali ke UI, Anda menyinkronkan kembali ke konteks permintaan. Ini mungkin berubah segera: yang baru ApiControllermemang memiliki HttpRequestMessagekonteks sebagai properti - jadi mungkin tidak diperlukan untuk mengalir melalui konteks SynchronizationContext- tapi saya belum tahu.
Stephen Cleary
62

Sunting: Umumnya mencoba untuk menghindari melakukan di bawah ini kecuali sebagai upaya terakhir untuk menghindari kebuntuan. Baca komentar pertama dari Stephen Cleary.

Perbaikan cepat dari sini . Alih-alih menulis:

Task tsk = AsyncOperation();
tsk.Wait();

Mencoba:

Task.Run(() => AsyncOperation()).Wait();

Atau jika Anda membutuhkan hasil:

var result = Task.Run(() => AsyncOperation()).Result;

Dari sumber (diedit agar sesuai dengan contoh di atas):

AsyncOperation sekarang akan dipanggil di ThreadPool, di mana tidak akan ada SynchronizationContext, dan kelanjutan yang digunakan di dalam AsyncOperation tidak akan dipaksa kembali ke utas pemanggilan.

Bagi saya ini sepertinya pilihan yang bisa digunakan karena saya tidak memiliki opsi untuk membuatnya async (yang saya lebih suka).

Dari sumber:

Pastikan bahwa menunggu dalam metode FooAsync tidak menemukan konteks untuk kembali ke marshal. Cara paling sederhana untuk melakukannya adalah dengan memanggil pekerjaan asinkron dari ThreadPool, seperti dengan membungkus doa dalam Task.Run, mis.

Int Sync () {return Task.Run (() => Library.FooAsync ()) .Hasil; }

FooAsync sekarang akan dipanggil di ThreadPool, di mana tidak akan ada SynchronizationContext, dan kelanjutan yang digunakan di dalam FooAsync tidak akan dipaksa kembali ke utas yang menggunakan Sync ().

Ykok
sumber
7
Mungkin ingin membaca kembali tautan sumber Anda; penulis merekomendasikan untuk tidak melakukan ini. Apakah itu bekerja? Ya, tetapi hanya dalam arti bahwa Anda menghindari kebuntuan. Solusi ini meniadakan semua manfaat asynckode pada ASP.NET, dan pada kenyataannya dapat menyebabkan masalah pada skala. BTW, ConfigureAwaittidak "memecah perilaku async yang tepat" dalam skenario apa pun; itu persis apa yang harus Anda gunakan dalam kode perpustakaan.
Stephen Cleary
2
Ini seluruh bagian pertama, berjudul dalam huruf tebal Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Seluruh bagian posting menjelaskan beberapa cara berbeda untuk melakukannya jika Anda benar - benar perlu .
Stephen Cleary
1
Menambahkan bagian yang saya temukan di sumber - saya akan menyerahkannya kepada pembaca di masa depan untuk memutuskan. Perhatikan bahwa Anda umumnya harus mencoba untuk menghindari melakukan hal ini dan hanya melakukannya sebagai pilihan terakhir (mis. Ketika menggunakan kode async Anda tidak memiliki kendali atas).
Ykok
3
Saya suka semua jawaban di sini dan seperti biasa .... semua didasarkan pada konteks (pun intended lol). Saya membungkus panggilan Async HttpClient dengan versi sinkron sehingga saya tidak dapat mengubah kode itu untuk menambahkan ConfigureAwait ke perpustakaan itu. Jadi untuk mencegah kebuntuan dalam produksi, saya membungkus panggilan Async di Task.Run. Jadi seperti yang saya pahami, ini akan menggunakan 1 utas tambahan per permintaan dan menghindari kebuntuan. Saya berasumsi bahwa untuk sepenuhnya mematuhi, saya harus menggunakan metode sinkronisasi WebClient. Itu banyak pekerjaan untuk dibenarkan jadi saya perlu alasan kuat untuk tidak bertahan dengan pendekatan saya saat ini.
samneric
1
Saya akhirnya membuat Metode Ekstensi untuk mengonversi Async ke Sinkronisasi. Saya membaca di sini di suatu tempat dengan cara yang sama kerangka Net melakukannya :. public static TResult RunSync <TResult> .GetResult (); }
samneric
10

Karena Anda menggunakan .Resultatau .Waitatau awaitini akan berakhir menyebabkan kebuntuan dalam kode Anda.

Anda dapat menggunakan ConfigureAwait(false)di asyncmetode untuk mencegah kebuntuan

seperti ini:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

Anda dapat menggunakan ConfigureAwait(false)sedapat mungkin untuk Jangan Memblokir Kode Async.

Hasan Fathi
sumber
2

Kedua sekolah ini tidak termasuk di dalamnya.

Berikut adalah skenario di mana Anda hanya perlu menggunakannya

   Task.Run(() => AsyncOperation()).Wait(); 

atau semacamnya

   AsyncContext.Run(AsyncOperation);

Saya memiliki tindakan MVC yang berada di bawah atribut transaksi basis data. Idenya adalah (mungkin) untuk memutar kembali semua yang dilakukan dalam tindakan jika terjadi kesalahan. Ini tidak memungkinkan pengalihan konteks, jika tidak, kemunduran atau komit transaksi akan gagal dengan sendirinya.

Perpustakaan yang saya butuhkan adalah async karena diharapkan untuk menjalankan async.

Satu-satunya pilihan. Jalankan sebagai panggilan sinkronisasi biasa.

Saya hanya mengatakan kepada masing-masing sendiri.

alex.peter
sumber
jadi Anda menyarankan opsi pertama dalam jawaban Anda?
Don Cheadle
1

Saya akan menempatkan ini di sini lebih untuk kelengkapan daripada relevansi langsung ke OP. Saya menghabiskan hampir sehari men-debug HttpClientpermintaan, bertanya-tanya mengapa saya tidak pernah mendapatkan balasan.

Akhirnya menemukan bahwa aku lupa untuk awaitpara asyncpanggilan lebih bawah panggilan stack.

Terasa seperti kehilangan titik koma.

Bondolin
sumber
-1

Saya mencari di sini:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

Dan di sini:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

Dan melihat:

Jenis ini dan anggotanya dimaksudkan untuk digunakan oleh kompiler.

Mengingat awaitversi ini berfungsi, dan apakah cara yang 'benar' dalam melakukan sesuatu, apakah Anda benar-benar membutuhkan jawaban untuk pertanyaan ini?

Pilihan saya adalah: Menyalahgunakan API .

yamen
sumber
Saya tidak memperhatikan hal itu, meskipun saya telah melihat bahasa lain di sekitar yang menunjukkan bahwa menggunakan API GetResult () adalah use-case yang didukung (dan diharapkan).
Benjamin Fox
1
Lebih jauh dari itu, jika Anda Test5Controller.Get()menolak untuk menghilangkan penunggu dengan yang berikut: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Perilaku yang sama dapat diamati.
Benjamin Fox