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

Resuelto Benjamin Fox asked hace 12 años • 7 respuestas

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 estancada en el WaitingForActivationestado.

Encontré 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 regresará.

Esto solo ocurre en ciertas circunstancias cuando se usa 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 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.

¿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();
    }
}
Benjamin Fox avatar Apr 27 '12 08:04 Benjamin Fox
Aceptado

Estás haciendo un mal uso de la API.

Esta es la situación: en ASP.NET, sólo un hilo puede manejar una solicitud a la vez. Puede realizar algún procesamiento paralelo si es necesario (tomando 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 .

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

Entonces, he aquí por qué test5falla:

  • Test5Controller.Getse ejecuta AsyncAwait_GetSomeDataAsync(dentro del contexto de solicitud de ASP.NET).
  • AsyncAwait_GetSomeDataAsyncse ejecuta HttpClient.GetAsync(dentro del contexto de solicitud de ASP.NET).
  • La solicitud HTTP se envía y HttpClient.GetAsyncdevuelve un archivo Task.
  • AsyncAwait_GetSomeDataAsyncespera el Task; como no está completo, AsyncAwait_GetSomeDataAsyncdevuelve un archivo incompleto Task.
  • Test5Controller.Get bloquea el hilo actual hasta que se Taskcomplete.
  • Llega la respuesta HTTP y se completa el Taskretorno .HttpClient.GetAsync
  • AsyncAwait_GetSomeDataAsyncintenta reanudarse dentro del contexto de solicitud de ASP.NET. Sin embargo, ya existe un hilo en ese contexto: el hilo bloqueado en Test5Controller.Get.
  • Punto muerto.

He aquí por qué los demás funcionan:

  • ( test1, test2y test3): Continuations_GetSomeDataAsyncprograma la continuación al grupo de subprocesos, fuera del contexto de solicitud de ASP.NET. Esto permite que la Taskdevolución Continuations_GetSomeDataAsyncse complete sin tener que volver a ingresar el contexto de la solicitud.
  • ( test4y test6): dado que Taskse espera , el hilo de solicitud de ASP.NET no está bloqueado. Esto permite AsyncAwait_GetSomeDataAsyncutilizar el contexto de solicitud de ASP.NET cuando esté listo para continuar.

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

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

De esa manera, obtiene ambos beneficios: la continuación (el resto del AsyncAwait_GetSomeDataAsyncmétodo) se ejecuta en un subproceso de grupo de subprocesos básico que no tiene que ingresar al contexto de solicitud de ASP.NET; y el controlador en sí lo es async(que no bloquea un hilo de solicitud).

Más información:

  • Mi publicación async/ awaitintroducción , que incluye una breve descripción de cómo Taskusan los camareros SynchronizationContext.
  • Las preguntas frecuentes sobre Async/Await , que detallan más los contextos. Consulte también Await, UI y puntos muertos. ¡Oh mi! lo cual se aplica aquí aunque esté en ASP.NET en lugar de en una interfaz de usuario, porque ASP.NET SynchronizationContextrestringe el contexto de solicitud a solo un subproceso a la vez.
  • Esta publicación en el foro de MSDN .
  • Stephen Toub hace una demostración de este punto muerto (usando una interfaz de usuario) , al igual que Lucian Wischik .

Actualización 13 de julio de 2012: se incorporó esta respuesta en una publicación de blog .

Stephen Cleary avatar Apr 27 '2012 13:04 Stephen Cleary

Editar: generalmente trate de evitar hacer lo siguiente, excepto como ú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();

Intentar:

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):

AsyncOperation ahora se invocará en ThreadPool, donde no habrá un SynchronizationContext, y las continuaciones utilizadas dentro de AsyncOperation no se forzarán a regresar al subproceso que lo invoca.

Para mí, esto parece una opción utilizable ya que no tengo la opción de hacerlo asíncrono por completo (que preferiría).

De la fuente:

Asegúrese de que await en el método FooAsync no encuentre un contexto al que volver a reunir. La forma más sencilla de hacerlo es invocar el trabajo asincrónico desde ThreadPool, por ejemplo envolviendo la invocación en 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 utilizadas dentro de FooAsync no se forzarán a regresar al subproceso que invoca Sync().

Ykok avatar Nov 01 '2013 13:11 Ykok