Editar: esta pregunta parece que podría ser el mismo problema, pero no tiene respuestas ...
Editar: en el caso de prueba 5, la tarea parece estar atascada en el WaitingForActivation
estado.
Me encontré con un comportamiento extraño al usar System.Net.Http.HttpClient en .NET 4.5, donde "esperar" el resultado de una llamada a (por ejemplo) httpClient.GetAsync(...)
nunca volverá.
Esto solo ocurre en ciertas circunstancias cuando se usa la nueva funcionalidad de lenguaje asíncrono / espera y la API de tareas: el código siempre parece funcionar cuando se usan solo continuaciones.
Aquí hay un código que reproduce el problema: colóquelo en un nuevo "proyecto MVC 4 WebApi" en Visual Studio 11 para exponer los siguientes puntos finales GET:
/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6
Cada uno de los puntos finales aquí devuelve los mismos datos (los encabezados de respuesta de stackoverflow.com) excepto /api/test5
que nunca se completa.
¿Me he encontrado con un error en la clase HttpClient, o estoy haciendo un mal uso de la API de alguna manera?
Código para reproducir:
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();
}
}
fuente
HttpClient.GetAsync(...)
?Respuestas:
Estás haciendo un mal uso de la API.
Aquí está la situación: en ASP.NET, solo un hilo puede manejar una solicitud a la vez. Puede hacer un procesamiento paralelo si es necesario (tomar prestados subprocesos adicionales del grupo de subprocesos), pero solo un subproceso tendría el contexto de solicitud (los subprocesos adicionales no tienen el contexto de solicitud).
Esto es administrado por ASP.NET
SynchronizationContext
.Por defecto, cuando
await
aTask
, el método se reanuda en un capturadoSynchronizationContext
(o un capturadoTaskScheduler
, si no haySynchronizationContext
). Normalmente, esto es justo lo que desea: una acción de controlador asíncrono seráawait
algo y, cuando se reanude, se reanudará con el contexto de la solicitud.Entonces, aquí está el por qué
test5
falla:Test5Controller.Get
se ejecutaAsyncAwait_GetSomeDataAsync
(dentro del contexto de solicitud ASP.NET).AsyncAwait_GetSomeDataAsync
se ejecutaHttpClient.GetAsync
(dentro del contexto de solicitud ASP.NET).HttpClient.GetAsync
devuelve un mensaje incompleto.Task
.AsyncAwait_GetSomeDataAsync
espera elTask
; ya que no está completo,AsyncAwait_GetSomeDataAsync
devuelve un incompletoTask
.Test5Controller.Get
bloquea el hilo actual hasta queTask
complete.Task
devolución deHttpClient.GetAsync
.AsyncAwait_GetSomeDataAsync
intenta reanudar dentro del contexto de solicitud ASP.NET. Sin embargo, ya hay un hilo en ese contexto: el hilo bloqueadoTest5Controller.Get
.He aquí por qué funcionan los otros:
test1
,test2
ytest3
):Continuations_GetSomeDataAsync
programa la continuación del grupo de subprocesos, fuera del contexto de solicitud ASP.NET. Esto permite que elTask
devuelto seContinuations_GetSomeDataAsync
complete sin tener que volver a ingresar el contexto de la solicitud.test4
ytest6
): comoTask
se espera , el subproceso de solicitud ASP.NET no está bloqueado. Esto permiteAsyncAwait_GetSomeDataAsync
usar el contexto de solicitud ASP.NET cuando está listo para continuar.Y aquí están las mejores prácticas:
async
métodos de "biblioteca" , useConfigureAwait(false)
siempre que sea posible. En su caso, esto cambiaríaAsyncAwait_GetSomeDataAsync
para servar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
Task
s; estáasync
todo el camino hacia abajo. En otras palabras, use enawait
lugar deGetResult
(Task.Result
yTask.Wait
también debe reemplazarse porawait
).De esa manera, obtienes ambos beneficios: la continuación (el resto del
AsyncAwait_GetSomeDataAsync
método) se ejecuta en un subproceso de grupo de subprocesos básico que no tiene que ingresar el contexto de solicitud ASP.NET; y el controlador en sí esasync
(que no bloquea un hilo de solicitud).Más información:
async
/await
introducción , que incluye una breve descripción de cómoTask
usan los camarerosSynchronizationContext
.SynchronizationContext
restringe el contexto de solicitud a solo un hilo a la vez.Actualización 2012-07-13: incorporó esta respuesta en una publicación de blog .
fuente
SynchroniztaionContext
que explique que solo puede haber un hilo en el contexto para alguna solicitud? Si no, creo que debería haberlo.SynchronizationContext
proporciona algunas funcionalidades importantes: fluye el contexto de solicitud. Esto incluye todo tipo de cosas, desde autenticación hasta cookies y cultura. Entonces, en ASP.NET, en lugar de volver a sincronizar con la interfaz de usuario, vuelve a sincronizar con el contexto de la solicitud. Esto puede cambiar en breve: lo nuevoApiController
tiene unHttpRequestMessage
contexto como propiedad, por lo que es posible que no sea necesario queSynchronizationContext
lo haga fluir , pero aún no lo sé.Editar: generalmente trate de evitar hacer lo siguiente, excepto como un último esfuerzo para evitar puntos muertos. Lea el primer comentario de Stephen Cleary.
Solución rápida desde aquí . En lugar de escribir:
Tratar:
O si necesitas un resultado:
Desde la fuente (editado para que coincida con el ejemplo anterior):
Para mí, esto parece una opción útil ya que no tengo la opción de hacerlo asíncrono todo el tiempo (lo que preferiría).
De la fuente:
fuente
async
código en ASP.NET y, de hecho, puede causar problemas a escala. Por cierto,ConfigureAwait
no "rompe el comportamiento asíncrono adecuado" en ningún escenario; es exactamente lo que debes usar en el código de la biblioteca.Avoid Exposing Synchronous Wrappers for Asynchronous Implementations
. Todo el resto de la publicación explica algunas formas diferentes de hacerlo si es absolutamente necesario .Dado que está utilizando
.Result
o.Wait
oawait
esto terminará causando un punto muerto en su código.puede usar
ConfigureAwait(false)
enasync
métodos para prevenir el punto muertoMe gusta esto:
fuente
Estas dos escuelas no son realmente excluyentes.
Aquí está el escenario donde simplemente tienes que usar
o algo así
Tengo una acción MVC que está bajo el atributo de transacción de base de datos. La idea era (probablemente) revertir todo lo hecho en la acción si algo sale mal. Esto no permite el cambio de contexto, de lo contrario, la reversión o confirmación de transacción fallará.
La biblioteca que necesito es asíncrona, ya que se espera que se ejecute de forma asíncrona.
La unica opcion. Ejecútelo como una llamada de sincronización normal.
Solo digo a cada uno lo suyo.
fuente
Voy a poner esto aquí más por completo que por relevancia directa para el OP. Pasé casi un día depurando una
HttpClient
solicitud, preguntándome por qué nunca recibiría una respuesta.Finalmente descubrí que había olvidado
await
laasync
llamada más abajo en la pila de llamadas.Se siente tan bien como perder un punto y coma.
fuente
Estoy mirando aqui:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
Y aquí:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx
Y viendo:
Teniendo en cuenta que la
await
versión funciona y es la forma "correcta" de hacer las cosas, ¿realmente necesita una respuesta a esta pregunta?Mi voto es: mal uso de la API .
fuente
Test5Controller.Get()
para eliminar al camarero con lo siguiente:var task = AsyncAwait_GetSomeDataAsync(); return task.Result;
Se puede observar el mismo comportamiento.