HttpClient.GetAsync (...) nunca regresa cuando se usa await / async
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 estado WaitingForActivation
.
He encontrado algún comportamiento extraño usando System.Net.Http.HttpClient en.NET 4.5 - donde "esperando" 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 un código que reproduce el problema: colóquelo en un nuevo "proyecto WebAPI MVC 4" en Visual Studio 11 para exponer los siguientes extremos GET:
/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6
Cada uno de los endpoints aquí devuelve los mismos datos (los encabezados de respuesta de stackoverflow.com) excepto /api/test5
que nunca se completa.
He encontrado un error en la clase HttpClient, o estoy usando mal la API en algunos ¿camino?
Código a 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();
}
}
5 answers
Está 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 algún procesamiento paralelo si es necesario (tomar prestados subprocesos adicionales del grupo de subprocesos), pero solo un subproceso tendría el contexto de la solicitud (los subprocesos adicionales no tienen el contexto de la solicitud).
Esto es gestionado por el ASP.NET SynchronizationContext
.
De forma predeterminada, cuando await
a Task
, el método se reanuda en un SynchronizationContext
capturado (o un capturado TaskScheduler
, si no hay SynchronizationContext
). Normalmente, esto es justo lo que desea: una acción de controlador asíncrono await
hará algo, y cuando se reanuda, se reanuda con el contexto de la solicitud.
Entonces, he aquí por qué test5
falla:
-
Test5Controller.Get
ejecutaAsyncAwait_GetSomeDataAsync
(dentro de la ASP.NET contexto de solicitud). -
AsyncAwait_GetSomeDataAsync
ejecutaHttpClient.GetAsync
(dentro de la ASP.NET contexto de solicitud). - Se envía la solicitud HTTP, y
HttpClient.GetAsync
devuelve unTask
incompleto. -
AsyncAwait_GetSomeDataAsync
espera laTask
; dado que no está completo,AsyncAwait_GetSomeDataAsync
devuelve unTask
incompleto. -
Test5Controller.Get
bloquea el hilo actual hasta queTask
se complete. - La respuesta HTTP entra, y el
Task
devuelto porHttpClient.GetAsync
se completa. -
AsyncAwait_GetSomeDataAsync
intentos de reanudar dentro de la ASP.NET contexto de solicitud. Sin embargo, ya hay un hilo en ese contexto: el hilo bloqueado enTest5Controller.Get
. - Punto muerto.
He aquí por qué los otros funcionan:
- (
test1
,test2
, ytest3
):Continuations_GetSomeDataAsync
programa la continuación al grupo de subprocesos, fuera de el ASP.NET contexto de solicitud. Esto permite que elTask
devuelto porContinuations_GetSomeDataAsync
se complete sin tener que volver a ingresar el contexto de la solicitud. - (
test4
andtest6
): Since theTask
is awaited , the ASP.NET el hilo de solicitud no está bloqueado. Esto permite aAsyncAwait_GetSomeDataAsync
utilizar el ASP.NET solicite el contexto cuando esté listo para continuar.
Y aquí están las mejores prácticas:
- En sus métodos de" biblioteca "
async
, useConfigureAwait(false)
siempre que sea posible. En su caso, esto cambiaríaAsyncAwait_GetSomeDataAsync
avar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- No bloquees en
Task
s; esasync
todo el camino hacia abajo. En otras palabras, useawait
en lugar deGetResult
(Task.Result
andTask.Wait
should also be replaced withawait
).
De esa manera, obtienes 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 el ASP.NET contexto de la solicitud; y el controlador en sí mismo es async
(que no bloquea un subproceso de solicitud).
Más información:
- Mi
async
/await
intro post , que incluye una breve descripción de cómoTask
los awaiters usanSynchronizationContext
. - El Async/Await FAQ, que entra en más detalle sobre los contextos. Ver también Await, y UI, y deadlocks! ¡Oh, Dios! que hace aplicar aquí a pesar de que usted está en ASP.NET en lugar de una interfaz de usuario, porque el ASP.NET
SynchronizationContext
restringe la solicitud contexto a solo un hilo a la vez. - This MSDN forum post.
- Stephen Toub muestra 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.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-28 13:39:42
Editar: Generalmente trata de evitar hacer lo siguiente, excepto como un último esfuerzo para evitar bloqueos. Lee el primer comentario de Stephen Cleary.
Solución rápida desde aquí. En lugar de escribir:
Task tsk = AsyncOperation();
tsk.Wait();
Intenta:
Task.Run(() => AsyncOperation()).Wait();
O si necesitas un resultado:
var result = Task.Run(() => AsyncOperation()).Result;
De la fuente (editada para que coincida con el ejemplo anterior):
Ahora se invocará la AsincOperación en el ThreadPool, donde no será un SynchronizationContext, y las continuaciones utilizadas dentro de AsincOperación no será forzado de nuevo al hilo de invocación.
Para mí esto parece una opción utilizable, ya que no tengo la opción de hacerlo asíncrono todo el camino (que preferiría).
De la fuente:
Asegúrese de que el await en el método FooAsync no encuentre un contexto para marshal vuelve. La forma más sencilla de hacerlo es invocar el trabajo asíncrono desde el ThreadPool, como envolver el invocación en una tarea.Ejecutar, por ejemplo,
Int Sync() { devolver Tarea.Ejecutar(() => Biblioteca.FooAsync ()).Result; }
FooAsync ahora se invocará en el ThreadPool, donde no habrá un SynchronizationContext, y las continuaciones utilizadas dentro de FooAsync no será forzado a volver al hilo que está invocando Sync().
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-15 10:07:41
Ya que estás usando .Result
o .Wait
o await
esto terminará causando un punto muerto en tu código.
Puede usar ConfigureAwait(false)
en async
métodos para prevenir el punto muerto
Así:
var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
Puede usar
ConfigureAwait(false)
siempre que sea posible para No Bloquear el Código Asincrónico .
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-20 15:12:52
Estas dos escuelas no son realmente excluyentes.
Aquí está el escenario donde simplemente tienes que usar
Task.Run(() => AsyncOperation()).Wait();
O algo así como
AsyncContext.Run(AsyncOperation);
Tengo una acción MVC que está bajo 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 de la transacción o la confirmación fallarán por sí mismas.
La biblioteca que necesito es asincrónica como se espera que se ejecute asíncrono.
La única opción. Ejecútalo como una llamada de sincronización normal.
Solo estoy diciendo a cada uno lo suyo.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-17 17:18:32
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.
Considerando la versión await
funciona, y es la forma 'correcta' de hacer las cosas, ¿realmente necesitas una respuesta a esta pregunta?
Mi voto es: Mal Uso de la API.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-27 01:57:05