364 votes

HttpClient.GetAsync(...) ne retourne jamais lorsqu'on utilise await/async

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

Modifier: Dans le cas de test 5, la tâche semble être bloquée dans l'état 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 renverra jamais.

Cela ne se produit que dans certaines circonstances lors de l'utilisation de la nouvelle fonctionnalité de langage async/await et de l'API Tasks - le code semble toujours fonctionner lorsque seules les continuations sont utilisées.

Voici un code qui reproduit le problème - ajoutez ceci dans un nouveau "projet WebApi MVC 4" dans Visual Studio 11 pour exposer les points d'extrémité GET suivants :

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- ne se termine jamais
/api/test6

Chacun des points d'extrémité ici renvoie les mêmes données (les en-têtes de réponse de stackoverflow.com) sauf /api/test5 qui ne se termine jamais.

Ai-je rencontré un bug dans la classe HttpClient, ou est-ce que j'utilise mal l'API d'une manière ou d'une autre ?

Code pour reproduire :

public class BaseApiController : ApiController
{
    /// 
    /// Récupère des données en utilisant des 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.ToString());
    }

    /// 
    /// Récupère des données en utilisant 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.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// 
    /// Gère la tâche en utilisant Async/Await
    /// 
    public async Task Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// 
    /// Gère la tâche en bloquant le thread jusqu'à ce que la tâche se termine
    /// 
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

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

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// 
    /// Passe la tâche au contrôleur hôte
    /// 
    public Task Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// 
    /// Gère la tâche en utilisant Async/Await
    /// 
    public async Task Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// 
    /// Gère la tâche en bloquant le thread jusqu'à ce que la tâche se termine
    /// 
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

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

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// 
    /// Passe la tâche au contrôleur hôte
    /// 
    public Task Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}

2 votes

Il ne semble pas que ce soit le même problème, mais juste pour vous informer, il y a un bug MVC4 dans la version bêta concernant les méthodes asynchrones qui se terminent de manière synchrone - voir stackoverflow.com/questions/9627329/…

0 votes

Merci - je vais faire attention à cela. Dans ce cas, je pense que la méthode devrait toujours être asynchrone en raison de l'appel à HttpClient.GetAsync(...)?

516voto

Stephen Cleary Points 91731

Vous utilisez incorrectement l'API.

Voici la situation : en ASP.NET, seul un thread peut gérer une requête à la fois. Vous pouvez effectuer un traitement en parallèle si nécessaire (en empruntant des threads supplémentaires à partir du pool de threads), mais un seul thread aura le contexte de la requête (les threads supplémentaires n'ont pas le contexte de la requête).

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

Par défaut, lorsque vous await un 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 lorsqu'elle reprend, elle le fait avec le contexte de la requête.

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 requête ASP.NET).
  • La requête HTTP est envoyée, et HttpClient.GetAsync renvoie un Task non terminé.
  • AsyncAwait_GetSomeDataAsync attend le Task; comme il n'est pas complet, AsyncAwait_GetSomeDataAsync renvoie un Task non terminé.
  • Test5Controller.Get bloque le thread actuel jusqu'à ce que ce Task soit terminé.
  • La réponse HTTP arrive, et le Task retourné par HttpClient.GetAsync est terminé.
  • AsyncAwait_GetSomeDataAsync tente de reprendre dans le contexte de la requête ASP.NET. Cependant, il y a 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 continuation sur le pool de threads, en dehors du contexte de requête ASP.NET. Cela permet au Task retourné par Continuations_GetSomeDataAsync de se terminer sans avoir à réintégrer le contexte de la requête.
  • (test4 et test6) : Puisque le Task est attendu, le thread de requête 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 de "bibliothèque", utilisez ConfigureAwait(false) autant que possible. Dans votre cas, cela changerait AsyncAwait_GetSomeDataAsync pour être var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Ne bloquez pas sur des Tasks ; c'est de l'async tout au long. En d'autres termes, utilisez await plutôt que GetResult (Task.Result et Task.Wait doivent également être remplacés par await).

De cette manière, vous obtenez les deux avantages : la continuation (le reste de la méthode AsyncAwait_GetSomeDataAsync) s'exécute sur un thread de base du pool de threads qui n'a pas besoin de réintégrer le contexte de requête ASP.NET ; et le contrôleur lui-même est async (ce qui ne bloque pas un thread de requête).

Plus d'informations :

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

2 votes

Y a-t-il une documentation pour le SynchroniztaionContext ASP.NET qui explique qu'il ne peut y avoir qu'un seul thread dans le contexte pour une requête ? Sinon, je pense qu'il devrait y en avoir une.

9 votes

Il n'est documenté nulle part à ma connaissance.

13 votes

Merci - impressionnante réponse. La différence de comportement entre le code (apparemment) fonctionnellement identique est frustrante mais a du sens avec votre explication. Ce serait utile si le framework était capable de détecter de tels deadlocks et de lever une exception quelque part.

74voto

Ykok Points 185

Modifier : Essayer généralement d'éviter de faire ce qui suit, sauf en dernier recours pour éviter les impasses. Lisez le premier commentaire de Stephen Cleary.

Correctif rapide à partir d'ici. Au lieu d'écrire :

Tâche tsk = OpérationAsync();
tsk.Wait();

Essayez :

Task.Run(() => OpérationAsync()).Wait();

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

var result = Task.Run(() => OpérationAsync()).Result;

Depuis la source (modifié pour correspondre à l'exemple ci-dessus) :

OpérationAsync sera maintenant appelé sur le ThreadPool, où il n'y a pas de SynchronizationContext, et les continuations utilisées à l'intérieur d'OpérationAsync ne seront pas forcées à revenir au thread appelant.

Pour moi, cela semble être une option utilisable car je n'ai pas la possibilité de le rendre asynchrone tout le temps (ce que je préférerais).

Depuis la source :

Assurez-vous que l'await dans la méthode FooAsync ne trouve pas de contexte pour revenir. Le moyen le 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(() => Bibliothèque.FooAsync()).Result; }

FooAsync sera maintenant appelé 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 à revenir au thread qui appelle Sync().

7 votes

Peut-être vouloir relire votre lien source ; l'auteur recommande de ne pas faire cela. Est-ce que cela fonctionne ? Oui, mais seulement dans le sens où vous évitez le deadlock. Cette solution annule tous les avantages du code async sur ASP.NET, et peut en fait causer des problèmes à grande échelle. En passant, ConfigureAwait ne "casse pas le comportement asynchrone approprié" dans aucun scénario ; c'est exactement ce que vous devriez utiliser dans le code de bibliothèque.

0 votes

J'ai relu le lien source. Je n'ai pas trouvé à quoi vous faisiez référence. Mais si le chapitre "Exemple du monde réel" est mal interprété, cela pourrait donner l'impression qu'il recommande de ne pas le faire, alors qu'il parle en fait d'un cas complètement différent.

2 votes

C'est toute la première section, intitulée en gras Évitez d'exposer des wrappers synchrones pour des implémentations asynchrones. Le reste de l'article explique quelques moyens différents de le faire si vous en avez vraiment besoin.

21voto

Hasan Fathi Points 2182

Étant donné que vous utilisez .Result ou .Wait ou await, cela finira par provoquer un deadlock dans votre code.

vous pouvez utiliser ConfigureAwait(false) dans les méthodes async pour prévenir les deadlocks

comme ceci:

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

vous pouvez utiliser ConfigureAwait(false) partout où c'est possible afin de ne pas bloquer le code asynchrone.

3voto

alex.peter Points 119

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 qui est sous l'attribut de transaction de base de données. L'idée était (probablement) de tout annuler dans l'action si quelque chose se passe mal. Cela ne permet pas le changement de contexte, sinon l'annulation ou la validation de la transaction va échouer.

La bibliothèque dont j'ai besoin est asynchrone car elle est censée s'exécuter de manière asynchrone.

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

Je dis juste à chacun le sien.

0 votes

Donc, vous suggérez la première option dans votre réponse ?

0 votes

"La bibliothèque dont j'ai besoin est asynchrone car elle est censée s'exécuter de manière asynchrone." C'est là le véritable problème.

2voto

NargothBond Points 362

Je vais mettre ceci ici plus pour la complétude que pour la pertinence directe par rapport à l'OP. J'ai passé presque une journée à déboguer une demande HttpClient, me demandant pourquoi je ne recevais jamais de réponse.

J'ai enfin trouvé que j'avais oublié d'attendre l'appel async plus loin dans la pile d'appels.

Cela ressemble à peu près à oublier un point-virgule.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X