HttpClient.GetAsync (...) ne retourne jamais lorsque vous utilisez attente / async

Edit: Cette question on dirait que ça pourrait être le même problème, mais n'a pas de réponses...

Edit: dans le cas d'essai 5, la tâche semble être bloquée dans l'état WaitingForActivation .

j'ai rencontré un comportement étrange en utilisant le système.Net.Http.HttpClient dans .NET 4.5-où " en attente "du résultat d'un appel à (e.g.) httpClient.GetAsync(...) ne reviendra jamais.

cela ne se produit que dans certaines circonstances lors de l'utilisation de la nouvelle API async/wait language functionality and Tasks - le code semble toujours fonctionner lorsqu'il n'utilise que des continuations.

voici un code qui reproduit le problème-déposez-le dans un nouveau "MVC 4 WebApi project" dans Visual Studio 11 pour exposer les points de fin GET suivants:

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

chaque paramètre 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 bug dans la classe HttpClient, ou est-ce que j'abuse de 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();
    }
}
263
demandé sur Community 2012-04-27 05:28:07

5 réponses

vous utilisez mal L'API.

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

c'est géré par le ASP.NET SynchronizationContext .

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

donc, voici pourquoi test5 échoue:

  • Test5Controller.Get execute AsyncAwait_GetSomeDataAsync (within the ASP.NET demande cadre.)
  • AsyncAwait_GetSomeDataAsync execute HttpClient.GetAsync (within the ASP.NET contexte de la demande).
  • la requête HTTP est envoyée, et HttpClient.GetAsync renvoie un Task Non complété .
  • AsyncAwait_GetSomeDataAsync attend le Task ; comme il n'est pas complet, AsyncAwait_GetSomeDataAsync retourne un Task inachevé .
  • Test5Controller.Get blocs le thread courant jusqu'à ce que Task compléter.
  • la réponse HTTP est arrivée, et le Task retourné par HttpClient.GetAsync est terminé.
  • AsyncAwait_GetSomeDataAsync tentatives de reprendre dans le ASP.NET contexte de la demande. Cependant, il y a 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 prévoit la continuation du pool de fils, à l'extérieur de le ASP.NET contexte de la demande. Cela permet au Task retourné par Continuations_GetSomeDataAsync de compléter sans avoir à entrer de nouveau le contexte de la requête.
  • ( test4 et test6 ): puisque le Task est attendu , le ASP.NET le thread de requête n'est pas bloqué. Cela permet à AsyncAwait_GetSomeDataAsync d'utiliser le ASP.NET contexte de demande lorsqu'il est prêt à continuer.

et voici les meilleures pratiques:

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

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

plus d'informations:

mise à jour 2012-07-13: incorporé cette réponse dans un billet de blog .

399
répondu Stephen Cleary 2014-08-28 13:39:42

Edit: généralement essayer d'éviter de faire le ci-dessous sauf comme un dernier effort de fossé pour éviter des blocages. Lisez le premier commentaire de Stephen Cleary.

Quick fix de ici . Au lieu d'écrire:

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

, Essayez:

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

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

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

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

AsyncOperation va maintenant être appelée sur le pool de threads, où il n'y ne sera pas un SynchronizationContext, et les poursuites utilisé à l'intérieur d' d'Asyncopération ne sera pas forcé de revenir au fil d'invocation.

pour moi cela ressemble à une option utilisable puisque je n'ai pas l'option de faire async tout le chemin (ce que je préférerais).

à Partir de la source:

S'assurer que l'attente La méthode FooAsync ne trouve pas de contexte pour le maréchal de retour. La façon la plus simple de le faire est d'appeler la un travail asynchrone à partir du ThreadPool, par exemple en enveloppant le invocation dans une Tâche.Exécuter, par exemple

Int Sync() { de retour de la Tâche.Exécuter(() => Bibliothèque.FooAsync ()).Résultat;}

FooAsync sera maintenant invoqué sur le ThreadPool, où il n'y aura pas de SynchronizationContext, et les continuations utilisées à L'intérieur de FooAsync ne sera pas forcés de retourner dans le thread qui est en invoquant Sync().

51
répondu Ykok 2018-02-15 10:07:41

puisque vous utilisez .Result ou .Wait ou await cela finira par causer une impasse dans votre code.

vous pouvez utiliser ConfigureAwait(false) dans async les méthodes pour la prévention de blocage

comme ceci:

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

vous pouvez utiliser ConfigureAwait(false) dans la mesure du possible pour ne pas bloquer le Code Async .

3
répondu Hasan Fathi 2018-01-20 15:12:52

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 faire reculer tout ce qui avait été fait dans l'action si quelque chose tournait mal. Cela ne permet pas de changer de contexte, sinon la transaction sera annulée. m'.

la bibliothèque dont j'ai besoin est async car on s'attend à ce qu'elle tourne async.

la seule option. Courir comme une normale de synchronisation d'appel.

je dis juste à chacun le sien.

0
répondu alex.peter 2017-05-17 17:18:32

je regarde ici:

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

et ici:

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

et voir:

ce type et ses membres sont conçu pour une utilisation par le compilateur.

considérant que la version await fonctionne, et que c'est la "bonne" façon de faire les choses, Avez-vous vraiment besoin d'une réponse à cette question?

Mon vote est: mauvais usage de l'API .

-1
répondu yamen 2012-04-27 01:57:05