c# - metodos - public async await
HttpClient.GetAsync(…) nunca regresa cuando se usa await/async (5)
Edit: esta pregunta parece que podría ser el mismo problema, pero no tiene respuestas ...
Edición: en el caso de prueba 5, la tarea parece estar atascada en el estado WaitingForActivation .
Encontré algún 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 utiliza la nueva funcionalidad de lenguaje async / await y la API de tareas: el código siempre parece funcionar cuando se usan solo continuaciones.
Aquí hay algunos códigos que reproducen 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) a excepción de /api/test5 que nunca se completa.
¿He encontrado 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();
}
}
Edición: Generalmente, trate de evitar hacer lo siguiente, excepto como un último esfuerzo de zanja para evitar puntos muertos. Lee el primer comentario de Stephen Cleary.
Solución rápida desde here . En lugar de escribir:
Task tsk = AsyncOperation();
tsk.Wait();
Tratar:
Task.Run(() => AsyncOperation()).Wait();
O si necesitas un resultado:
var result = Task.Run(() => AsyncOperation()).Result;
Desde la fuente (editado para que coincida con el ejemplo anterior):
AsyncOperation ahora se invocará en ThreadPool, donde no habrá un SynchronizationContext, y las continuaciones usadas dentro de AsyncOperation no serán forzadas a regresar al hilo de invocación.
Para mí, esto parece una opción útil, ya que no tengo la opción de hacerlo asíncrono (lo que preferiría).
De la fuente:
Asegúrate de que la espera en el método FooAsync no encuentre un contexto al que volver. La forma más sencilla de hacerlo es invocar el trabajo asíncrono desde ThreadPool, por ejemplo, envolviendo la invocación en un Task.Run, por ejemplo
int Sync () {return Task.Run (() => Library.FooAsync ()). Resultado; }
FooAsync ahora se invocará en ThreadPool, donde no habrá un SynchronizationContext, y las continuaciones usadas dentro de FooAsync no serán forzadas a regresar al hilo que está invocando Sync ().
Estás haciendo 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 (pedir prestados subprocesos adicionales del grupo de subprocesos), pero solo un subproceso tendrá el contexto de solicitud (los subprocesos adicionales no tienen el contexto de solicitud).
Esto es administrado por ASP.NET SynchronizationContext .
De forma predeterminada, cuando await una Task , el método se reanuda en un SynchronizationContext capturado (o un TaskScheduler capturado, si no hay un SynchronizationContext ). Normalmente, esto es justo lo que desea: una acción de controlador asíncrono await algo y, cuando se reanude, se reanudará con el contexto de la solicitud.
Entonces, aquí está la razón por la cual test5 falla:
-
Test5Controller.GetejecutaAsyncAwait_GetSomeDataAsync(dentro del contexto de solicitud ASP.NET). -
AsyncAwait_GetSomeDataAsyncejecutaHttpClient.GetAsync(dentro del contexto de solicitud ASP.NET). - La solicitud HTTP se envía y
HttpClient.GetAsyncdevuelve unaTaskincompleta. -
AsyncAwait_GetSomeDataAsyncespera laTask; ya que no está completo,AsyncAwait_GetSomeDataAsyncdevuelve unaTaskincompleta. -
Test5Controller.Getbloquea el subproceso actual hasta que se complete laTask. - La respuesta HTTP entra, y la
Taskdevuelta porHttpClient.GetAsyncse completa. -
AsyncAwait_GetSomeDataAsyncintenta reanudarse dentro del contexto de solicitud ASP.NET. Sin embargo, ya existe un subproceso en ese contexto: el subproceso bloqueado enTest5Controller.Get. - Punto muerto.
He aquí por qué los otros trabajan:
- (
test1,test2ytest3):Continuations_GetSomeDataAsyncprograma la continuación al grupo de subprocesos, fuera del contexto de solicitud ASP.NET. Esto permite que laTaskdevuelta porContinuations_GetSomeDataAsynccomplete sin tener que volver a entrar en el contexto de la solicitud. - (
test4ytest6): como se espera laTask, no se bloquea el subproceso de solicitud ASP.NET. Esto permite queAsyncAwait_GetSomeDataAsyncuse el contexto de solicitud ASP.NET cuando esté listo para continuar.
Y aquí están las mejores prácticas:
- En los métodos
asyncsu "biblioteca", useConfigureAwait(false)siempre que sea posible. En su caso, esto cambiaríaAsyncAwait_GetSomeDataAsyncpara que seavar result = await httpClient.GetAsync("http://.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - No bloquee en la
Tasks; Esasynchasta el fondo. En otras palabras, useTask.Resultlugar deGetResult(Task.ResultyTask.Waittambién deben reemplazarse conawait).
De esa manera, obtiene ambos beneficios: la continuación (el resto del método AsyncAwait_GetSomeDataAsync ) se ejecuta en un subproceso de grupo de subprocesos básico que no tiene que ingresar al contexto de solicitud ASP.NET; y el controlador en sí es async (que no bloquea un subproceso de solicitud).
Más información:
- Mi post de introducción de
async/ await, que incluye una breve descripción de cómo los solicitantes deTaskusanSynchronizationContext. - Las preguntas frecuentes de Async / Await , que se detallan más en los contextos. ¡También vea Await, y UI, y puntos muertos! ¡Oh mi! lo que se aplica aquí aunque esté en ASP.NET en lugar de en una interfaz de usuario, ya que ASP.NET
SynchronizationContextrestringe el contexto de la solicitud a solo un hilo a la vez. - Esta publicación del foro de MSDN .
- Stephen Toub demuestra este punto muerto (usando una interfaz de usuario) , y también Lucian Wischik .
Actualización 2012-07-13: Incorporé esta respuesta en una publicación de blog .
Estas dos escuelas no son realmente excluyentes.
Aquí está el escenario donde simplemente tienes que usar
Task.Run(() => AsyncOperation()).Wait();
o algo como
AsyncContext.Run(AsyncOperation);
Tengo una acción MVC que está bajo el atributo de transacción de la base de datos. La idea era (probablemente) deshacer todo lo que se hace en la acción si algo sale mal. Esto no permite el cambio de contexto, de lo contrario, la reversión de la transacción o la confirmación fallarán.
La biblioteca que necesito es async, ya que se espera que se ejecute async.
La única opción. Ejecutarlo como una llamada de sincronización normal.
Solo estoy diciendo a cada uno lo suyo.
Estoy buscando aquí:
http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
Y aquí:
Y viendo:
Este tipo y sus miembros están destinados a ser utilizados por el compilador.
Teniendo en cuenta que la versión await 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 .
Ya que está utilizando .Result o .Wait o await esto terminará causando un punto muerto en su código.
puede usar ConfigureAwait(false) en métodos async para prevenir el interbloqueo
Me gusta esto:
var result = await httpClient.GetAsync("http://.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
puede utilizar
ConfigureAwait(false)siempre que sea posible para No bloquear el código asíncrono.