web-dev-qa-db-fra.com

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

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

Edit: 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 le 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 async/wait et 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 d'extrémité 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.

Ai-je rencontré un bogue dans la classe HttpClient ou est-ce que j'utilise mal 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();
    }
}
292
Benjamin Fox

Vous utilisez mal 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 du pool de threads), mais un seul thread aurait le contexte de demande (les threads supplémentaires ne possèdent pas le contexte de demande).

C'est géré par ASP.NET SynchronizationContext .

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

Alors, voici pourquoi test5 échoue:

  • Test5Controller.Get exécute AsyncAwait_GetSomeDataAsync (dans le contexte de la demande ASP.NET).
  • AsyncAwait_GetSomeDataAsync exécute HttpClient.GetAsync (dans le contexte de la demande ASP.NET).
  • La demande HTTP est envoyée et HttpClient.GetAsync renvoie un Task inachevé.
  • AsyncAwait_GetSomeDataAsync attend la Task; comme il n'est pas complet, AsyncAwait_GetSomeDataAsync renvoie un Task inachevé.
  • Test5Controller.Get bloque le thread actuel jusqu'à ce que Task soit terminé.
  • La réponse HTTP arrive et la Task renvoyée par HttpClient.GetAsync est terminée.
  • AsyncAwait_GetSomeDataAsync tente de reprendre dans le contexte de demande ASP.NET. Cependant, il existe déjà un fil dans ce contexte: le fil bloqué dans Test5Controller.Get.
  • Impasse.

Voici pourquoi les autres fonctionnent:

  • (test1, test2 et test3): Continuations_GetSomeDataAsync programme la suite du pool de threads, en dehors de l'ASP. Contexte de demande NET. Cela permet à la Task renvoyée par Continuations_GetSomeDataAsync de se terminer sans avoir à ressaisir le contexte de la demande.
  • (test4 et test6): Étant donné que la Task est attendue , le thread de demande ASP.NET n'est pas bloqué. Cela permet à AsyncAwait_GetSomeDataAsync d'utiliser le contexte de demande ASP.NET lorsqu'il est prêt à continuer.

Et voici les meilleures pratiques:

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

Ainsi, vous bénéficiez des deux avantages suivants: la suite (le reste de la méthode AsyncAwait_GetSomeDataAsync] est exécutée sur un thread de pool de threads de base qui ne doit pas nécessairement 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 requête).

Plus d'information:

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

441
Stephen Cleary

Edit: Essayez en général d’éviter les opérations ci-dessous, sauf comme dernier effort pour éviter les blocages. Lisez le premier commentaire de Stephen Cleary.

Correction rapide de 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;

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

AsyncOperation sera maintenant appelé sur le ThreadPool, où il n'y aura pas de SynchronizationContext, et les continuations utilisées dans AsyncOperation ne seront pas forcées à revenir au thread invoquant.

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

De la source:

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

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

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

57
Ykok

Puisque vous utilisez .Result ou .Wait ou await, cela provoquera un impasse dans votre code.

vous pouvez utiliser ConfigureAwait(false) dans async méthodes pour éviter les blocages

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 async.

6
Hasan Fathi

Ces deux écoles ne sont pas vraiment exclues.

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 revenir en arrière si tout se passait mal. Cela n'autorise pas la commutation de contexte, sinon l'annulation ou la validation d'une transaction échouera d'elle-même.

La bibliothèque dont j'ai besoin est asynchrone, car elle est supposée fonctionner en mode asynchrone.

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

Je dis juste à chacun la sienne.

1
alex.peter

Je vais mettre cela plus ici pour plus de complétude que de pertinence directe pour le PO. J'ai passé presque une journée à déboguer une demande HttpClient, en me demandant pourquoi je ne recevais jamais de réponse.

Finalement, j'ai découvert que j'avais oublié de await l'appel async plus bas dans la pile d'appels.

On se sent aussi bon que de manquer un point-virgule.

1
Bondolin

Je cherche 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.

Considérant que la version await fonctionne et constitue la "bonne" façon de procéder, avez-vous vraiment besoin d'une réponse à cette question?

Mon vote est: mal utiliser l'API .

0
yamen