HttpClient.GetAsync (…) ne retourne jamais lors de l’utilisation de wait / async

Edit: Cette question semble avoir le même problème, mais n’a pas de réponse …

Edit: Dans le WaitingForActivation test 5, la tâche semble être bloquée dans l’état WaitingForActivation .

J’ai rencontré certains comportements bizarres en utilisant System.Net.Http.HttpClient dans .NET 4.5 – où “attendre” le résultat d’un appel à (par exemple) httpClient.GetAsync(...) ne reviendra jamais.

Cela ne se produit que dans certaines circonstances lorsque vous utilisez la nouvelle fonctionnalité de langage asynchrone / wait et l’API Tasks – le code semble toujours fonctionner lorsque vous utilisez uniquement des continuations.

Voici un code qui reproduit le problème: déposez-le dans un nouveau projet “MVC 4 WebApi” dans Visual Studio 11 pour exposer les points de terminaison GET suivants:

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

Chacun des points de terminaison renvoie ici les mêmes données (les en-têtes de réponse de stackoverflow.com), à l’exception de /api/test5 qui ne se termine jamais.

Est-ce que j’ai rencontré un bogue dans la classe HttpClient ou est-ce que je mal utilise l’API?

Code à reproduire:

 public class BaseApiController : ApiController { ///  /// Resortingeves data using continuations ///  protected Task Continuations_GetSomeDataAsync() { var httpClient = new HttpClient(); var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return t.ContinueWith(t1 => t1.Result.Content.Headers.ToSsortingng()); } ///  /// Resortingeves data using async/await ///  protected async Task AsyncAwait_GetSomeDataAsync() { var httpClient = new HttpClient(); var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return result.Content.Headers.ToSsortingng(); } } public class Test1Controller : BaseApiController { ///  /// Handles task using Async/Await ///  public async Task Get() { var data = await Continuations_GetSomeDataAsync(); return data; } } public class Test2Controller : BaseApiController { ///  /// Handles task by blocking the thread until the task completes ///  public ssortingng Get() { var task = Continuations_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test3Controller : BaseApiController { ///  /// Passes the task back to the controller host ///  public Task Get() { return Continuations_GetSomeDataAsync(); } } public class Test4Controller : BaseApiController { ///  /// Handles task using Async/Await ///  public async Task Get() { var data = await AsyncAwait_GetSomeDataAsync(); return data; } } public class Test5Controller : BaseApiController { ///  /// Handles task by blocking the thread until the task completes ///  public ssortingng Get() { var task = AsyncAwait_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test6Controller : BaseApiController { ///  /// Passes the task back to the controller host ///  public Task Get() { return AsyncAwait_GetSomeDataAsync(); } } 

Vous utilisez mal l’API.

Voici la situation: dans ASP.NET, un seul thread peut gérer une requête à la fois. Vous pouvez effectuer un parallel processing si nécessaire (en empruntant des threads supplémentaires dans le pool de threads), mais un seul thread aurait le contexte de requête (les threads supplémentaires n’ont pas le contexte de requête).

Ceci est géré par ASP.NET SynchronizationContext .

Par défaut, lorsque vous await une Task , la méthode reprend sur un SynchronizationContext capturé (ou un TaskScheduler capturé, s’il n’y a pas de SynchronizationContext ). Normalement, c’est exactement ce que vous voulez: une action de contrôleur asynchrone await quelque chose, et quand elle reprend, elle reprend le contexte de la requête.

Donc, voici pourquoi test5 échoue:

  • Test5Controller.Get exécute AsyncAwait_GetSomeDataAsync (dans le contexte de la requête ASP.NET).
  • AsyncAwait_GetSomeDataAsync exécute HttpClient.GetAsync (dans le contexte de la demande ASP.NET).
  • La requête HTTP est envoyée et HttpClient.GetAsync renvoie une Task incomplète.
  • AsyncAwait_GetSomeDataAsync attend la Task ; comme il n’est pas complet, AsyncAwait_GetSomeDataAsync renvoie une Task incomplète.
  • Test5Controller.Get bloque le thread en cours jusqu’à la fin de cette Task .
  • La réponse HTTP entre et la Task renvoyée par HttpClient.GetAsync est terminée.
  • AsyncAwait_GetSomeDataAsync tente de reprendre dans le contexte de la requête ASP.NET. Cependant, il existe déjà un thread dans ce contexte: le thread bloqué dans Test5Controller.Get .
  • Impasse.

Voici pourquoi les autres fonctionnent:

  • ( test1 , test2 et test3 ): Continuations_GetSomeDataAsync planifie la poursuite du pool de threads, en dehors du contexte de la requête ASP.NET. Cela permet à la Task renvoyée par Continuations_GetSomeDataAsync de se terminer sans avoir à ressaisir le contexte de la demande.
  • ( test6 et test6 ): Comme la Task est attendue , le thread de demande ASP.NET n’est pas bloqué. Cela permet à AsyncAwait_GetSomeDataAsync d’utiliser le contexte de requête ASP.NET lorsqu’il est prêt à continuer.

Et voici les meilleures pratiques:

  1. Dans vos méthodes async “library”, utilisez ConfigureAwait(false) chaque fois que possible. Dans votre cas, cela changerait AsyncAwait_GetSomeDataAsync pour var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Ne bloquez pas la Task s; c’est async tout en bas. En d’autres termes, utilisez GetResult au lieu de GetResult ( Task.Result et Task.Wait doivent également être remplacés par await ).

De cette façon, vous obtenez les deux avantages: la suite (le rest de la méthode AsyncAwait_GetSomeDataAsync ) est exécutée sur un thread de pool de threads de base qui ne doit pas entrer dans le contexte de la requête ASP.NET; et le contrôleur lui-même est async (qui ne bloque pas un thread de demande).

Plus d’information:

  • Mon post d’intro async / waiting , qui comprend une brève description de la manière dont les serveurs de Task utilisent SynchronizationContext .
  • La FAQ Async / Await , qui décrit plus en détail les contextes. Voir aussi Await, et l’interface utilisateur, et les impasses! Oh mon! qui s’applique ici même si vous êtes dans ASP.NET plutôt que dans une interface utilisateur, car ASP.NET SynchronizationContext restreint le contexte de la requête à un seul thread à la fois.
  • Ce post sur le forum MSDN .
  • Stephen Toub montre cette impasse (à l’aide d’une interface utilisateur) , tout comme Lucian Wischik .

Mise à jour 2012-07-13: Incorporer cette réponse dans un article de blog .

Modifier: essayez généralement d’éviter de faire les opérations ci-dessous, sauf en dernier recours pour éviter les blocages. Lisez le premier commentaire de Stephen Cleary.

Solution rapide d’ ici . Au lieu d’écrire:

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

Essayer:

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

Ou si vous avez besoin d’un résultat:

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

De la source (édité pour correspondre à l’exemple ci-dessus):

AsyncOperation va maintenant être appelé sur le ThreadPool, où il n’y aura pas de SynchronizationContext, et les continuations utilisées dans AsyncOperation ne seront pas renvoyées au thread appelant.

Pour moi, cela ressemble à une option utilisable car je n’ai pas la possibilité de la rendre asynchrone jusqu’à la fin (ce que je préférerais).

De la source:

Assurez-vous que l’attente dans la méthode FooAsync ne trouve pas de contexte à regrouper. La méthode la plus simple consiste à appeler le travail asynchrone à partir de ThreadPool, par exemple en encapsulant l’appel dans un Task.Run, par exemple

int Sync () {return Task.Run (() => Library.FooAsync ()). Résultat; }

FooAsync va maintenant être appelé sur le ThreadPool, où il n’y aura pas de SynchronizationContext, et les continuations utilisées dans FooAsync ne seront pas renvoyées au thread qui appelle Sync ().

Puisque vous utilisez .Result ou .Wait ou await ceci finira par provoquer un blocage dans votre code.

vous pouvez utiliser ConfigureAwait(false) dans les méthodes async pour empêcher le blocage

comme ça:

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

Vous pouvez utiliser ConfigureAwait(false) mesure du possible pour Don’t Block Async Code.

Ces deux écoles n’excluent pas vraiment.

Voici le scénario où vous devez simplement utiliser

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

ou quelque chose comme

  AsyncContext.Run(AsyncOperation); 

J’ai une action MVC sous l’atsortingbut de transaction de firebase database. L’idée était (probablement) de tout annuler dans l’action si quelque chose se passe mal. Cela ne permet pas la commutation de contexte, sinon la restauration ou la validation de la transaction va échouer.

La bibliothèque dont j’ai besoin est asynchrone car elle est censée exécuter un asynchrone.

La seule option. Exécutez-le comme un appel de synchronisation normal.

Je dis juste à chacun son propre.

Je regarde ici:

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

Et ici:

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

Et en voyant:

Ce type et ses membres sont destinés à être utilisés par le compilateur.

Considérant que la version d’ await fonctionne, et est la bonne façon de faire les choses, avez-vous vraiment besoin d’une réponse à cette question?

Mon vote est le suivant: mal utiliser l’API .