HttpClient.GetAsync (…), await / async kullanırken asla geri dönmez


315

Düzenleme: Bu soru aynı sorun olabilir gibi görünüyor, ancak yanıt yok ...

Düzenleme: Sınama durumunda 5. görev WaitingForActivationdurumun içinde kalmış gibi görünür .

.NET 4.5'te System.Net.Http.HttpClient kullanarak bazı garip davranışlarla karşılaştım - burada "beklemek" çağrısı sonucu (örneğin) httpClient.GetAsync(...)asla dönmeyecek.

Bu, yalnızca yeni eşzamansız / bekliyor dil işlevselliği ve Görevler API'sı kullanılırken belirli durumlarda gerçekleşir - kod her zaman yalnızca devamları kullanırken işe yarar.

Sorunu yeniden üreten bazı kod İşte - aşağıdaki GET uç noktalarını göstermek için Visual Studio 11 yeni bir "MVC 4 WebApi projesine" bırakın:

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

Buradaki uç noktaların her biri aynı verileri (stackoverflow.com'dan yanıt başlıkları) /api/test5hiçbir zaman tamamlanmadığı sürece döndürür .

HttpClient sınıfında bir hatayla mı karşılaştım yoksa API'yı bir şekilde kötüye mi kullanıyorum?

Çoğaltılacak kod:

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();
    }
}

2
Aynı sorun gibi görünmüyor, ancak sadece bunu bildiğinizden emin olmak için beta WRT zaman uyumsuz yöntemlerinde senkronize olarak tamamlanan bir MVC4 hatası var - bkz. Stackoverflow.com/questions/9627329/…
James Manning

Teşekkürler - Buna dikkat edeceğim. Bu durumda ben yöntem her zaman çünkü çağrı asenkron olması gerektiğini düşünüyorum HttpClient.GetAsync(...)?
Benjamin Fox

Yanıtlar:


468

API'yı yanlış kullanıyorsunuz.

Durum şu şekildedir: ASP.NET'te bir kerede yalnızca bir iş parçacığı isteği işleyebilir. Gerekirse bazı paralel işlemler yapabilirsiniz (iş parçacığı havuzundan ek iş parçacıkları ödünç alma), ancak yalnızca bir iş parçacığının istek içeriği olur (ek iş parçacıklarının istek içeriği yoktur).

Bu, ASP.NET tarafından yönetilirSynchronizationContext .

Eğer Varsayılan olarak, awaitbir Task, yöntem yakalanan üzerinde devam eder SynchronizationContext(veya yakalanan TaskSchedulerhayır varsa, SynchronizationContext). Normalde, bu tam olarak istediğiniz şeydir: eşzamansız bir denetleyici eylemi bir awaitşey olur ve devam ettiğinde istek bağlamıyla devam eder.

İşte neden test5başarısız oluyor:

  • Test5Controller.Getyürütür AsyncAwait_GetSomeDataAsync(ASP.NET istek bağlamında).
  • AsyncAwait_GetSomeDataAsyncyürütür HttpClient.GetAsync(ASP.NET istek bağlamında).
  • HTTP isteği gönderilir ve HttpClient.GetAsynctamamlanmamış bir değer döndürür Task.
  • AsyncAwait_GetSomeDataAsyncbekliyor Task; tamamlanmadığından, AsyncAwait_GetSomeDataAsynctamamlanmamış bir değer döndürür Task.
  • Test5Controller.Get Tasktamamlanıncaya kadar geçerli iş parçacığını engeller .
  • HTTP yanıtı gelir ve Tasktarafından döndürülen HttpClient.GetAsyncişlem tamamlanır.
  • AsyncAwait_GetSomeDataAsyncASP.NET istek bağlamında devam ettirmeye çalışır. Ancak, bu bağlamda zaten bir iş parçacığı var: iş parçacığı engellendi Test5Controller.Get.
  • Kilitlenme.

Diğerleri neden çalışıyor:

  • ( test1, test2ve test3): ASP.NET istek bağlamı dışındaContinuations_GetSomeDataAsync iş parçacığı havuzuna devam zamanlar . Bu, döndürülen tarafından istek bağlamını yeniden girmek zorunda kalmadan tamamlanmasını sağlar.TaskContinuations_GetSomeDataAsync
  • ( test4Ve test6): yana Taskolan beklenen , ASP.NET istek iplik engellenmez. Bu, AsyncAwait_GetSomeDataAsyncdevam etmeye hazır olduğunda ASP.NET istek bağlamının kullanılmasına izin verir .

Ve işte en iyi uygulamalar:

  1. "Kütüphane" asyncyöntemlerinizde ConfigureAwait(false)mümkün olduğunca kullanın . Senin durumunda bu değişirdi AsyncAwait_GetSomeDataAsyncolmakvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. TaskS üzerindeki engelleme ; her şey asyncyolunda. Başka bir deyişle, awaityerine kullanın GetResult( Task.Resultve Task.Waitbununla değiştirilmelidir await).

Bu şekilde, her iki avantajı elde edersiniz: devam ( AsyncAwait_GetSomeDataAsyncyöntemin geri kalanı ), ASP.NET istek bağlamını girmek zorunda olmayan bir temel iş parçacığı havuzu iş parçacığında çalıştırılır; ve denetleyicinin kendisi async(bir istek iş parçacığını engellemez).

Daha fazla bilgi:

2012-07-13 Güncellemesi: Bu yanıtı bir blog yayınına ekledi .


2
ASP.NET SynchroniztaionContextiçin, bazı istek bağlamında yalnızca bir iş parçacığı olabileceğini açıklayan bazı belgeler var mı ? Değilse, olması gerektiğini düşünüyorum.
svick

8
AFAIK hiçbir yerde belgelenmemiştir.
Stephen Cleary

10
Teşekkürler - harika yanıt. (Görünüşe göre) işlevsel olarak özdeş kod arasındaki davranış farkı sinir bozucu ama açıklamanızla mantıklı. Çerçevenin bu tür kilitlenmeleri tespit edebilmesi ve bir yerde bir istisna oluşturabilmesi yararlı olacaktır.
Benjamin Fox

3
Bir asp.net bağlamında .ConfigureAwait (false) kullanılmasının önerilmediği durumlar var mı? Bana her zaman kullanılmalı ve sadece kullanıcı arayüzü bağlamında kullanılmamalıdır çünkü kullanıcı arayüzüyle senkronize etmeniz gerekir. Yoksa konuyu özlüyor muyum?
AlexGad

3
ASP.NET SynchronizationContextbazı önemli işlevler sağlar: istek bağlamını akar. Bu, kimlik doğrulamasından çerezlere ve kültüre kadar her türlü şeyi içerir. ASP.NET'te, kullanıcı arabirimiyle eşitlemek yerine, istek bağlamıyla eşitlenirsiniz. Yeni: Bu kısa süre değişebilir ApiControllerbir var HttpRequestMessageo kadar - bir özellik olarak bağlam olabilir aracılığıyla bağlam akış gerekmeyecektir SynchronizationContext- ama henüz bilmiyorum.
Stephen Cleary

62

Düzenleme: Genellikle kilitlenmeleri önlemek için son hendek çaba dışında aşağıdakileri yapmaktan kaçının. Stephen Cleary'nin ilk yorumunu okuyun.

Buradan hızlı düzeltme . Yazmak yerine:

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

Deneyin:

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

Veya bir sonuca ihtiyacınız varsa:

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

Kaynaktan (yukarıdaki örneğe uyacak şekilde düzenlendi):

AsyncOperation şimdi bir SynchronizationContext olmayacak olan ThreadPool'da çağrılır ve AsyncOperation içinde kullanılan süreklilikler çağıran iş parçacığına geri gönderilmez.

Benim için bu kullanılabilir bir seçenek gibi görünüyor çünkü ben tamamen async yapma seçeneği yok (ki ben tercih ederim).

Kaynaktan:

FooAsync yönteminde beklemenin, mareşal için bir bağlam bulamadığından emin olun. Bunu yapmanın en basit yolu, çağrıyı bir Görev'e sarmak gibi bir eşzamansız işi ThreadPool'dan çağırmaktır, ör.

int Sync () {return Task.Run (() => Library.FooAsync ()). Sonuç; }

FooAsync şimdi bir SynchronizationContext olmayacak olan ThreadPool'da çağrılır ve FooAsync içinde kullanılan süreklilikler Sync () 'i çağıran iş parçacığına geri gönderilmez.


7
Kaynak bağlantınızı tekrar okumak isteyebilir; Yazar önerir değil bunu. Çalışıyor mu? Evet, ancak sadece kilitlenmeden kaçınmanız anlamında. Bu çözüm , ASP.NET'teki kodun tüm avantajlarını ortadan kaldırır asyncve aslında ölçekte sorunlara neden olabilir. BTW, ConfigureAwaithiçbir senaryoda "uygun zaman uyumsuz davranışı bozmaz"; tam olarak kütüphane kodunda kullanmanız gereken şeydir.
Stephen Cleary

2
İlk bölümün tamamı kalın yazılmıştır Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Gönderinin geri kalanı, kesinlikle ihtiyacınız varsa bunu yapmanın birkaç farklı yolunu açıklıyor .
Stephen Cleary

1
Kaynakta bulduğum bölümü ekledim - karar vermek için gelecekteki okuyuculara bırakacağım. Genelde bunu yapmaktan kaçınmaya çalışmanız ve bunu yalnızca son bir hendek tesisi olarak yapmanız gerektiğini unutmayın (yani async kodu kullanırken üzerinde kontrolünüz yoktur).
Ykok

3
Burada tüm cevapları seviyorum ve her zaman olduğu gibi .... hepsi bağlam (pun amaçlı lol) dayanmaktadır. Ben HttpClient Async çağrıları senkronize bir sürüm ile sarma bu yüzden ConfigureAwait bu kitaplığa eklemek için bu kodu değiştiremezsiniz. Üretimdeki kilitlenmeleri önlemek için, Async çağrılarını bir Görev'e kaydırıyorum. Anladığım kadarıyla, bu istek başına 1 ekstra iplik kullanacak ve kilitlenmeyi önleyecektir. Tamamen uyumlu olması için WebClient'in senkronizasyon yöntemlerini kullanmam gerektiğini varsayıyorum. Bu haklı çıkarmak için çok iş, bu yüzden mevcut yaklaşımımla uyuşmamak için zorlayıcı bir nedene ihtiyacım olacak.
samneric

1
Async'i Sync'e dönüştürmek için bir Uzantı Yöntemi oluşturdum. Burada bir yerde .Net çerçevesinin yaptığı gibi okudum: public static TResult RunSync <TResult> (bu Func <Görev <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric

10

Kullandığınız yana .Resultya .Waitya awaitbu bir neden sona erecek kilitlenme kodunuzda.

Kullanabileceğiniz ConfigureAwait(false)içinde asyncyöntemleri kilitlenme önlenmesi

bunun gibi:

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

ConfigureAwait(false)Asenkron Kodunu Engelleme için mümkün olan her yerde kullanabilirsiniz .


2

Bu iki okul gerçekten dışlanmıyor.

İşte sadece kullanmak zorunda olduğunuz senaryo

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

ya da benzeri bir şey

   AsyncContext.Run(AsyncOperation);

Veritabanı işlem özniteliği altında bir MVC eylem var. Fikir (muhtemelen) bir şeyler ters giderse eylemde yapılan her şeyi geri almaktı. Bu, içerik değiştirmeye izin vermez, aksi takdirde işlem geri alma veya kesinleştirme başarısız olur.

İhtiyacım olan kitaplık, zaman uyumsuz olarak çalışması beklendiği için zaman uyumsuzdur.

Tek seçenek. Normal bir senkronizasyon çağrısı olarak çalıştırın.

Sadece her birine kendi söylüyorum.


yani cevabınızdaki ilk seçeneği mi öneriyorsunuz?
Don Cheadle

1

Bunu tam olarak OP ile doğrudan ilgili olmaktan ziyade buraya koyacağım. Neredeyse HttpClienthiç yanıt alamadım, neden hiç yanıt alamadığımı merak ettim.

Sonunda unutmuştum bulundu çağrı yığını altındaki çağrı.awaitasync

Noktalı virgül eksik kadar iyi hissettiriyor.


-1

Buraya bakıyorum:

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

Ve burada:

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

Ve görüyorum:

Bu tür ve üyeleri derleyici tarafından kullanılmak üzere tasarlanmıştır.

Düşünüldüğünde awaitversiyon eserleri ve şeyleri yapmanın 'doğru' yol, gerçekten bu soruya bir cevap gerekiyor?

Benim oyum : API'yi yanlış kullanmak .


GetResult () API kullanımının desteklenen (ve beklenen) bir kullanım örneği olduğunu belirten başka bir dil gördüm.
Benjamin Fox

1
Bunun Test5Controller.Get()yanı sıra, bekleyiciyi aşağıdakilerle ortadan kaldırmayı tercih ederseniz: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Aynı davranış gözlemlenebilir.
Benjamin Fox
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.