HttpClient.GetAsync (…) nunca vuelve 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 bloqueada 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 del lenguaje async / await y la API de tareas: el código siempre parece funcionar cuando se usan solo las 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) a excepción de /api/test5 que nunca se completa.

¿He encontrado un error en la clase HttpClient, o estoy haciendo un uso indebido de la API de alguna manera?

Código para reproducir:

 public class BaseApiController : ApiController { ///  /// Retrieves data using continuations ///  protected Task Continuations_GetSomeDataAsync() { var httpClient = new HttpClient(); var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString()); } ///  /// Retrieves data using async/await ///  protected async Task 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 { ///  /// Handles task using Async/Await ///  public async Task Get() { var data = await Continuations_GetSomeDataAsync(); return data; } } public class Test2Controller : BaseApiController { ///  /// Handles task by blocking the thread until the task completes ///  public string Get() { var task = Continuations_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test3Controller : BaseApiController { ///  /// Passes the task back to the controller host ///  public Task Get() { return Continuations_GetSomeDataAsync(); } } public class Test4Controller : BaseApiController { ///  /// Handles task using Async/Await ///  public async Task Get() { var data = await AsyncAwait_GetSomeDataAsync(); return data; } } public class Test5Controller : BaseApiController { ///  /// Handles task by blocking the thread until the task completes ///  public string Get() { var task = AsyncAwait_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test6Controller : BaseApiController { ///  /// Passes the task back to the controller host ///  public Task Get() { return AsyncAwait_GetSomeDataAsync(); } } 

Estás haciendo un uso indebido de la API.

Aquí está la situación: en ASP.NET, solo un hilo puede manejar una solicitud a la vez. Puede hacer un parallel processing si es necesario (tomando en préstamo 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 SynchronizationContext ). Normalmente, esto es justo lo que quiere: una acción de controlador asíncrono await algo, y cuando se reanude, se reanudará con el contexto de solicitud.

Entonces, aquí está la razón por la cual test5 falla:

  • Test5Controller.Get ejecuta AsyncAwait_GetSomeDataAsync (dentro del contexto de solicitud de ASP.NET).
  • AsyncAwait_GetSomeDataAsync ejecuta HttpClient.GetAsync (dentro del contexto de solicitud de ASP.NET).
  • La solicitud HTTP se envía y HttpClient.GetAsync devuelve una Task incompleta.
  • AsyncAwait_GetSomeDataAsync espera la Task ; ya que no está completo, AsyncAwait_GetSomeDataAsync devuelve una Task incompleta.
  • Test5Controller.Get bloquea el hilo actual hasta que la Task finalice.
  • La respuesta HTTP entra y la Task devuelta por HttpClient.GetAsync se completa.
  • AsyncAwait_GetSomeDataAsync intenta reanudar dentro del contexto de solicitud de ASP.NET. Sin embargo, ya hay un hilo en ese contexto: el hilo bloqueado en Test5Controller.Get .
  • Punto muerto.

He aquí por qué los otros funcionan:

  • ( test1 , test2 y test3 ): Continuations_GetSomeDataAsync planifica la continuación del grupo de subprocesos, fuera del contexto de solicitud de ASP.NET. Esto permite que la Task devuelta por Continuations_GetSomeDataAsync complete sin tener que volver a ingresar al contexto de solicitud.
  • ( test4 y test6 ): dado que se espera la Task , el hilo de solicitud de ASP.NET no está bloqueado. Esto permite que AsyncAwait_GetSomeDataAsync use el contexto de solicitud de ASP.NET cuando esté listo para continuar.

Y aquí están las mejores prácticas:

  1. En sus métodos async “biblioteca”, use ConfigureAwait(false) siempre que sea posible. En su caso, esto cambiaría AsyncAwait_GetSomeDataAsync para ser var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. No bloquear en Task s; es async todo el camino hacia abajo. En otras palabras, use Task.Result lugar de GetResult ( Task.Result y Task.Wait también debe reemplazarse con await ).

De esta forma, obtendrá los dos beneficios: la continuación (el rest del método AsyncAwait_GetSomeDataAsync ) se ejecuta en un subproceso básico del grupo de subprocesos que no tiene que ingresar al contexto de solicitud de ASP.NET; y el controlador en sí es async (que no bloquea un hilo de solicitud).

Más información:

  • Mi intro post / async / await, que incluye una breve descripción de cómo Task awaiters usa SynchronizationContext .
  • Preguntas frecuentes sobre Async / Await , que detalla los contextos. ¡También vea Await, UI y deadlocks! ¡Oh mi! que se aplica aquí aunque esté en ASP.NET en lugar de en una IU, porque ASP.NET SynchronizationContext restringe el contexto de la solicitud a solo un hilo a la vez.
  • Esta publicación en el foro de MSDN .
  • Stephen Toub muestra este punto muerto (usando una IU) , y también Lucian Wischik .

Actualización 13-07-2012: Incorpora esta respuesta en una publicación de blog .

Edición: generalmente trate de evitar hacer lo siguiente excepto como último esfuerzo para evitar interlockings. Lee el primer comentario de Stephen Cleary.

Solución rápida desde aquí . En lugar de escribir:

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

Tratar:

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

O si necesita un resultado:

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

Desde la fuente (editado para que coincida con el ejemplo anterior):

Ahora se invocará AsyncOperation en ThreadPool, donde no habrá un SynchronizationContext, y las continuaciones utilizadas dentro de AsyncOperation no serán forzadas nuevamente a la cadena invocante.

Para mí, esto parece una opción utilizable, ya que no tengo la opción de hacer que sea asincrónico todo el camino (que yo preferiría).

De la fuente:

Asegúrese de que la espera en el método FooAsync no encuentre un contexto al que recurrir. La forma más sencilla de hacerlo es invocar el trabajo asincrónico desde ThreadPool, como al envolver la invocación en una Task.Run, por ejemplo

int Sync () {return Task.Run (() => Library.FooAsync ()). Resultado; }

Ahora FooAsync se invocará en ThreadPool, donde no habrá SynchronizationContext, y las continuaciones usadas dentro de FooAsync no serán forzadas de nuevo a la cadena que invoca Sync ().

Como está utilizando .Result o .Wait o await esto terminará .Wait su código.

puede usar ConfigureAwait(false) en métodos async para evitar un punto muerto

Me gusta esto:

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

puede usar ConfigureAwait(false) siempre que sea posible para Do Not Block Async Code.

Estas dos escuelas no son realmente excluyentes.

Aquí está el escenario donde simplemente tiene que usar

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

o algo así 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) 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.

La biblioteca que necesito es asincrónica, ya que se espera que se ejecute de manera sincronizada.

La única opción. Ejecútelo como una llamada de sincronización normal.

Solo digo a cada uno lo que es suyo.

Estoy buscando aquí:

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:

Este tipo y sus miembros están destinados a ser utilizados por el comstackdor.

Considerando que la versión de 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 .