HttpClient.GetAsync (...) nunca regresa cuando se usa await / async

315

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 WaitingForActivationestado.

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/test5que 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();
    }
}
Benjamin Fox
fuente
2
No parece ser el mismo problema, pero solo para asegurarse de que lo sepa, hay un error MVC4 en los métodos asíncronos beta WRT que se completan sincrónicamente - consulte stackoverflow.com/questions/9627329/…
James Manning
Gracias, tendré cuidado con eso. En este caso, creo que el método siempre debe ser asíncrono debido a la llamada a HttpClient.GetAsync(...)?
Benjamin Fox

Respuestas:

468

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.NETSynchronizationContext .

Por defecto, cuando awaita Task, el método se reanuda en un capturado SynchronizationContext(o un capturado TaskScheduler, si no hay SynchronizationContext). Normalmente, esto es justo lo que desea: una acción de controlador asíncrono será awaitalgo y, cuando se reanude, se reanudará con el contexto de la solicitud.

Entonces, aquí está el por qué test5falla:

  • Test5Controller.Getse ejecuta AsyncAwait_GetSomeDataAsync(dentro del contexto de solicitud ASP.NET).
  • AsyncAwait_GetSomeDataAsyncse ejecuta HttpClient.GetAsync(dentro del contexto de solicitud ASP.NET).
  • La solicitud HTTP se envía y HttpClient.GetAsyncdevuelve un mensaje incompleto.Task .
  • AsyncAwait_GetSomeDataAsyncespera el Task; ya que no está completo, AsyncAwait_GetSomeDataAsyncdevuelve un incompleto Task.
  • Test5Controller.Get bloquea el hilo actual hasta queTask complete.
  • La respuesta HTTP entra y se completa la Taskdevolución de HttpClient.GetAsync.
  • AsyncAwait_GetSomeDataAsyncintenta reanudar dentro del contexto de solicitud ASP.NET. Sin embargo, ya hay un hilo en ese contexto: el hilo bloqueado Test5Controller.Get.
  • Punto muerto.

He aquí por qué funcionan los otros:

  • ( test1, test2y test3): Continuations_GetSomeDataAsyncprograma la continuación del grupo de subprocesos, fuera del contexto de solicitud ASP.NET. Esto permite que el Taskdevuelto se Continuations_GetSomeDataAsynccomplete sin tener que volver a ingresar el contexto de la solicitud.
  • ( test4y test6): como Taskse 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:

  1. En sus asyncmétodos de "biblioteca" , use ConfigureAwait(false)siempre que sea posible. En su caso, esto cambiaría AsyncAwait_GetSomeDataAsyncpara servar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. No bloquees en Tasks; está asynctodo el camino hacia abajo. En otras palabras, use en awaitlugar de GetResult( Task.Resulty Task.Waittambién debe reemplazarse por await).

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í es async(que no bloquea un hilo de solicitud).

Más información:

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

Stephen Cleary
fuente
2
¿Existe alguna documentación para ASP.NET SynchroniztaionContextque explique que solo puede haber un hilo en el contexto para alguna solicitud? Si no, creo que debería haberlo.
svick
8
No está documentado en ningún lugar AFAIK.
Stephen Cleary
10
Gracias, increíble respuesta. La diferencia de comportamiento entre el código (aparentemente) funcionalmente idéntico es frustrante, pero tiene sentido con su explicación. Sería útil si el marco fuera capaz de detectar tales puntos muertos y generar una excepción en alguna parte.
Benjamin Fox
3
¿Hay situaciones en las que NO se recomienda usar .ConfigureAwait (false) en un contexto asp.net? Me parece que siempre debe usarse y que solo en un contexto de UI no debe usarse, ya que debe sincronizarse con la UI. ¿O me estoy perdiendo el punto?
AlexGad
3
ASP.NET SynchronizationContextproporciona 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 nuevo ApiControllertiene un HttpRequestMessagecontexto como propiedad, por lo que es posible que no sea necesario que SynchronizationContextlo haga fluir , pero aún no lo sé.
Stephen Cleary
62

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:

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 utilizadas dentro de AsyncOperation no se verán obligadas a volver al hilo de invocación.

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:

Asegúrese de que la espera en el método FooAsync no encuentre un contexto para volver a ordenar. La forma más sencilla de hacerlo es invocar el trabajo asincrónico desde el ThreadPool, como envolviendo la invocación en una tarea.

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

FooAsync ahora se invocará en ThreadPool, donde no habrá un SynchronizationContext, y las continuaciones utilizadas dentro de FooAsync no se verán obligadas a volver al hilo que invoca Sync ().

Ykok
fuente
77
Es posible que desee volver a leer su enlace de origen; El autor recomienda no hacer esto. ¿Funciona? Sí, pero solo en el sentido de que evitas el punto muerto. Esta solución niega todos los beneficios del asynccódigo en ASP.NET y, de hecho, puede causar problemas a escala. Por cierto, ConfigureAwaitno "rompe el comportamiento asíncrono adecuado" en ningún escenario; es exactamente lo que debes usar en el código de la biblioteca.
Stephen Cleary
2
Es toda la primera sección, titulada en negrita Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Todo el resto de la publicación explica algunas formas diferentes de hacerlo si es absolutamente necesario .
Stephen Cleary
1
Agregué la sección que encontré en la fuente. Dejaré que los futuros lectores decidan. Tenga en cuenta que generalmente debe tratar de evitar hacer esto y hacerlo solo como último recurso (es decir, cuando usa un código asíncrono sobre el que no tiene control).
Ykok
3
Me gustan todas las respuestas aquí y como siempre ... todas están basadas en el contexto (juego de palabras, jajaja). Estoy envolviendo las llamadas Async de HttpClient con una versión síncrona, así que no puedo cambiar ese código para agregar ConfigureAwait a esa biblioteca. Entonces, para evitar los puntos muertos en la producción, estoy envolviendo las llamadas asíncronas en una Task.Run. Entonces, según tengo entendido, esto va a usar 1 hilo adicional por solicitud y evita el punto muerto. Supongo que para ser completamente compatible, necesito usar los métodos de sincronización de WebClient. Eso es mucho trabajo para justificar, así que necesitaré una razón convincente para no seguir con mi enfoque actual.
samneric
1
Terminé creando un Método de extensión para convertir Async a Sync. Leí aquí en alguna parte, es de la misma manera que lo hace .Net Framework: public static TResult RunSync <TResult> (this Func <Task <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric
10

Dado que está utilizando .Resulto .Waito awaitesto terminará causando un punto muerto en su código.

puede usar ConfigureAwait(false)en asyncmétodos para prevenir el 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 No bloquear el código asincrónico.

Hasan Fathi
fuente
2

Estas dos escuelas no son realmente excluyentes.

Aquí está el escenario donde simplemente tienes que usar

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

o algo así

   AsyncContext.Run(AsyncOperation);

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.

alex.peter
fuente
¿Estás sugiriendo la primera opción en tu respuesta?
Don Cheadle
1

Voy a poner esto aquí más por completo que por relevancia directa para el OP. Pasé casi un día depurando una HttpClientsolicitud, preguntándome por qué nunca recibiría una respuesta.

Finalmente descubrí que había olvidado awaitla asyncllamada más abajo en la pila de llamadas.

Se siente tan bien como perder un punto y coma.

Bondolin
fuente
-1

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:

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

Teniendo en cuenta que la awaitversió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 .

yamen
fuente
No me había dado cuenta de eso, aunque he visto otro lenguaje que indica que el uso de la API GetResult () es un caso de uso compatible (y esperado).
Benjamin Fox
1
Además de eso, si refactoriza Test5Controller.Get()para eliminar al camarero con lo siguiente: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Se puede observar el mismo comportamiento.
Benjamin Fox