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

315

Edit: Cette question semble être le même problème, mais n'a pas de réponses ...

Edit: dans le cas de test 5, la tâche semble bloquée WaitingForActivation.

J'ai rencontré un comportement étrange 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 lors de l'utilisation de la nouvelle fonctionnalité de langage asynchrone / attente et de l'API Tâches - 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), sauf pour ceux /api/test5qui ne se terminent jamais.

Ai-je rencontré un bogue dans la classe HttpClient ou suis-je en train de mal utiliser l'API d'une manière ou d'une autre?

Code à reproduire:

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
la source
2
Il ne semble pas s'agir du même problème, mais juste pour vous en assurer, il existe un bogue MVC4 dans les méthodes asynchrones bêta WRT qui se terminent de manière synchrone - voir stackoverflow.com/questions/9627329/…
James Manning
Merci - je ferai attention à ça. Dans ce cas, je pense que la méthode doit toujours être asynchrone à cause de l'appel à HttpClient.GetAsync(...)?
Benjamin Fox

Réponses:

468

Vous utilisez abusivement l'API.

Voici la situation: dans ASP.NET, un seul thread peut gérer une demande à la fois. Vous pouvez effectuer un traitement parallèle si nécessaire (emprunter des threads supplémentaires au pool de threads), mais un seul thread aurait le contexte de la demande (les threads supplémentaires n'ont pas le contexte de la demande).

Ceci est géré par ASP.NETSynchronizationContext .

Par défaut, lorsque vous awaita Task, la méthode reprend sur une capture SynchronizationContext(ou une capture TaskScheduler, s'il n'y en a pas SynchronizationContext). Normalement, c'est exactement ce que vous voulez: une action de contrôleur asynchrone fera awaitquelque chose, et quand elle reprendra, elle reprendra avec le contexte de la demande.

Alors, voici pourquoi test5échoue:

  • Test5Controller.Gets'exécute AsyncAwait_GetSomeDataAsync(dans le contexte de la demande ASP.NET).
  • AsyncAwait_GetSomeDataAsyncs'exécute HttpClient.GetAsync(dans le contexte de la demande ASP.NET).
  • La requête HTTP est envoyée et HttpClient.GetAsyncrenvoie une requête incomplète Task.
  • AsyncAwait_GetSomeDataAsyncattend le Task; car il n'est pas complet, AsyncAwait_GetSomeDataAsyncrenvoie un inachevé Task.
  • Test5Controller.Get bloque le thread actuel jusqu'à ce qu'il se Tasktermine.
  • La réponse HTTP entre, et le Taskretourné par HttpClient.GetAsyncest terminé.
  • AsyncAwait_GetSomeDataAsynctente de reprendre dans le contexte de la demande ASP.NET. Cependant, il existe déjà un thread dans ce contexte: le thread bloqué Test5Controller.Get.
  • Impasse.

Voici pourquoi les autres fonctionnent:

  • ( test1,, test2et test3): Continuations_GetSomeDataAsyncplanifie la continuation vers le pool de threads, en dehors du contexte de demande ASP.NET. Cela permet au Taskretourné de Continuations_GetSomeDataAsyncse terminer sans avoir à ressaisir le contexte de la demande.
  • ( test4et test6): Étant donné que le Taskest attendu , le thread de demande ASP.NET n'est pas bloqué. Cela permet AsyncAwait_GetSomeDataAsyncd'utiliser le contexte de demande ASP.NET lorsqu'il est prêt à continuer.

Et voici les meilleures pratiques:

  1. Dans vos asyncméthodes de "bibliothèque" , utilisez ConfigureAwait(false)autant que possible. Dans votre cas, cela changerait AsyncAwait_GetSomeDataAsyncpour êtrevar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Ne bloquez pas sur Tasks; c'est asynctout en bas. En d'autres termes, utilisez awaitplutôt que GetResult( Task.Resultet Task.Waitdevrait également être remplacé par await).

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

Plus d'information:

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

Stephen Cleary
la source
2
Existe-t-il une documentation pour ASP.NET SynchroniztaionContextqui explique qu'il ne peut y avoir qu'un seul thread dans le contexte pour une demande? Sinon, je pense qu'il devrait y en avoir.
svick
8
Il n'est documenté nulle part AFAIK.
Stephen Cleary
10
Merci - réponse impressionnante . La différence de comportement entre du code (apparemment) fonctionnellement identique est frustrante mais a du sens avec votre explication. Il serait utile que le framework soit capable de détecter de tels blocages et de lever une exception quelque part.
Benjamin Fox
3
Existe-t-il des situations où l'utilisation de .ConfigureAwait (false) dans un contexte asp.net n'est PAS recommandée? Il me semble qu'il doit toujours être utilisé et que ce n'est que dans un contexte d'interface utilisateur qu'il ne doit pas être utilisé car vous devez vous synchroniser avec l'interface utilisateur. Ou est-ce que je manque le point?
AlexGad
3
L'ASP.NET SynchronizationContextfournit quelques fonctionnalités importantes: il coule le contexte de la demande. Cela inclut toutes sortes de choses, de l'authentification aux cookies en passant par la culture. Ainsi, dans ASP.NET, au lieu de synchroniser à nouveau avec l'interface utilisateur, vous effectuez une synchronisation avec le contexte de la demande. Cela peut changer sous peu: le nouveau ApiControllera un HttpRequestMessagecontexte en tant que propriété - il n'est donc peut- être pas nécessaire de le faire circuler SynchronizationContext- mais je ne sais pas encore.
Stephen Cleary
62

Edit: essayez généralement d'éviter de faire ce qui suit, sauf comme un dernier effort pour éviter les blocages. Lisez le premier commentaire de Stephen Cleary.

Solution rapide à partir 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 sera désormais invoqué sur le ThreadPool, où il n'y aura pas de SynchronizationContext, et les continuations utilisées à l'intérieur d'AsyncOperation ne seront pas forcées de revenir au thread appelant.

Pour moi, cela ressemble à une option utilisable car je n'ai pas la possibilité de le faire asynchroniser complètement (ce que je préférerais).

De la source:

Assurez-vous que l'attente dans la méthode FooAsync ne trouve pas de contexte vers lequel le marshaler. La façon la plus simple de le faire est d'appeler le travail asynchrone à partir du ThreadPool, par exemple en enveloppant l'invocation dans un Task.Run, par exemple

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

FooAsync sera désormais invoqué sur le ThreadPool, où il n'y aura pas de SynchronizationContext, et les continuations utilisées à l'intérieur de FooAsync ne seront pas forcées de revenir au thread qui appelle Sync ().

Ykok
la source
7
Pourrait vouloir relire votre lien source; l'auteur recommande de ne pas le faire. Est-ce que ça marche? Oui, mais uniquement dans le sens où vous évitez l'impasse. Cette solution annule tous les avantages du asynccode sur ASP.NET et peut en fait causer des problèmes à grande échelle. BTW, ConfigureAwaitne "casse pas le comportement asynchrone approprié" dans aucun scénario; c'est exactement ce que vous devez utiliser dans le code de la bibliothèque.
Stephen Cleary
2
C'est toute la première section, intitulée en gras Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Le reste du message explique plusieurs façons de le faire si vous en avez absolument besoin .
Stephen Cleary
1
Ajout de la section que j'ai trouvée dans la source - je laisse le choix aux futurs lecteurs. Notez que vous devez généralement essayer d'éviter cela et ne le faire qu'en dernier recours (c'est-à-dire lorsque vous utilisez du code asynchrone que vous n'avez pas le contrôle).
Ykok
3
J'aime toutes les réponses ici et comme toujours .... elles sont toutes basées sur le contexte (jeu de mots lol). J'encapsule les appels Async de HttpClient avec une version synchrone donc je ne peux pas changer ce code pour ajouter ConfigureAwait à cette bibliothèque. Donc, pour éviter les blocages en production, j'encapsule les appels Async dans un Task.Run. Donc, si je comprends bien, cela va utiliser 1 thread supplémentaire par demande et évite l'impasse. Je suppose que pour être complètement conforme, je dois utiliser les méthodes de synchronisation de WebClient. C'est beaucoup de travail à justifier, donc j'ai besoin d'une raison impérieuse pour ne pas m'en tenir à mon approche actuelle.
samneric
1
J'ai fini par créer une méthode d'extension pour convertir Async en synchronisation. J'ai lu ici quelque part de la même manière que le framework .Net: public statique TResult RunSync <TResult> (ce Func <Task <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric
10

Puisque vous utilisez .Resultou .Waitou awaitcela finira par provoquer un blocage dans votre code.

vous pouvez utiliser ConfigureAwait(false)dans les asyncméthodes pour empêcher l'impasse

comme ça:

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

vous pouvez utiliser ConfigureAwait(false)autant que possible pour Ne pas bloquer le code asynchrone.

Hasan Fathi
la source
2

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'attribut de transaction de base de données. L'idée était (probablement) de faire reculer tout ce qui se passait dans l'action en cas de problème. Cela ne permet pas le changement de contexte, sinon la restauration ou la validation de transaction échouera d'elle-même.

La bibliothèque dont j'ai besoin est asynchrone car elle devrait s'exécuter asynchrone.

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

Je dis juste à chacun son propre.

alex.peter
la source
vous proposez donc la première option dans votre réponse?
Don Cheadle
1

Je vais mettre cela ici pour plus d'exhaustivité que de pertinence directe pour le PO. J'ai passé près d'une journée à déboguer unHttpClient demande, me demandant pourquoi je n'obtenais jamais de réponse.

Enfin trouvé que j'avais oublié de awaitlaasync appel plus bas dans la pile des appels.

Ça fait presque comme manquer un point-virgule.

Bondolin
la source
-1

Je regarde ici:

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

Et ici:

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

Et en voyant:

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

Étant donné que la awaitversion fonctionne et est la «bonne» façon de faire les choses, avez-vous vraiment besoin d'une réponse à cette question?

Mon vote est: abuser de l'API .

yamen
la source
Je ne l'avais pas remarqué, bien que j'aie vu un autre langage autour qui indique que l'utilisation de l'API GetResult () est un cas d'utilisation pris en charge (et attendu).
Benjamin Fox
1
De plus, si vous refactorisez Test5Controller.Get()pour éliminer l'attente avec ce qui suit: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Le même comportement peut être observé.
Benjamin Fox