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();
}
}
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
executeAsyncAwait_GetSomeDataAsync
(within the ASP.NET demande cadre.) -
AsyncAwait_GetSomeDataAsync
executeHttpClient.GetAsync
(within the ASP.NET contexte de la demande). - la requête HTTP est envoyée, et
HttpClient.GetAsync
renvoie unTask
Non complété . -
AsyncAwait_GetSomeDataAsync
attend leTask
; comme il n'est pas complet,AsyncAwait_GetSomeDataAsync
retourne unTask
inachevé . -
Test5Controller.Get
blocs le thread courant jusqu'à ce queTask
compléter. - la réponse HTTP est arrivée, et le
Task
retourné parHttpClient.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é dansTest5Controller.Get
. - impasse.
Voici pourquoi les autres fonctionnent:
- (
test1
,test2
, ettest3
):Continuations_GetSomeDataAsync
prévoit la continuation du pool de fils, à l'extérieur de le ASP.NET contexte de la demande. Cela permet auTask
retourné parContinuations_GetSomeDataAsync
de compléter sans avoir à entrer de nouveau le contexte de la requête. - (
test4
ettest6
): puisque leTask
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:
- dans votre "bibliothèque"
async
méthodes, utilisezConfigureAwait(false)
chaque fois que possible. Dans votre cas, cela changeraitAsyncAwait_GetSomeDataAsync
envar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- Ne pas bloquer sur
Task
s; c'estasync
tout le chemin vers le bas. En d'autres termes, utiliserawait
au lieu deGetResult
(Task.Result
etTask.Wait
doit également être remplacé parawait
).
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:
- Mon
async
/await
intro post , qui comprend une brève description de la façon dontTask
awaiters useSynchronizationContext
. - The Async/wait FAQ , qui va plus en détail sur les contextes. Voir Aussi attendre, et UI, et les impasses! Oh, mon! qui ne s'appliquent ici, même si vous êtes dans ASP.NET plutôt qu'un UI, parce que le ASP.NET
SynchronizationContext
restreint le contexte de la requête à un seul thread à la fois. - Ce MSDN post sur le forum .
- de Stephen Toub démos de cette impasse (à l'aide d'une INTERFACE utilisateur , et afin de ne Lucian Wischik .
mise à jour 2012-07-13: incorporé cette réponse dans un billet de blog .
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().
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 .
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.
je regarde ici:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter (v=V110).aspx
et ici:
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 .