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();
    }
}
Author: Community, 2012-04-27

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 ejecuta AsyncAwait_GetSomeDataAsync (dentro de la ASP.NET contexto de solicitud).
  • AsyncAwait_GetSomeDataAsync ejecuta HttpClient.GetAsync (dentro de la ASP.NET contexto de solicitud).
  • Se envía la solicitud HTTP, y HttpClient.GetAsync devuelve un Task incompleto.
  • AsyncAwait_GetSomeDataAsync espera la Task; dado que no está completo, AsyncAwait_GetSomeDataAsync devuelve un Task incompleto.
  • Test5Controller.Get bloquea el hilo actual hasta que Task se complete.
  • La respuesta HTTP entra, y el Task devuelto por HttpClient.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 en Test5Controller.Get.
  • Punto muerto.

He aquí por qué los otros funcionan:

  • (test1, test2, y test3): Continuations_GetSomeDataAsync programa la continuación al grupo de subprocesos, fuera de el ASP.NET contexto de solicitud. Esto permite que el Task devuelto por Continuations_GetSomeDataAsync se complete sin tener que volver a ingresar el contexto de la solicitud.
  • (test4 and test6): Since the Task is awaited , the ASP.NET el hilo de solicitud no está bloqueado. Esto permite a AsyncAwait_GetSomeDataAsync utilizar el ASP.NET solicite el contexto cuando esté listo para continuar.

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

  1. En sus métodos de" biblioteca " async, use ConfigureAwait(false) siempre que sea posible. En su caso, esto cambiaría AsyncAwait_GetSomeDataAsync a var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. No bloquees en Task s; es async todo el camino hacia abajo. En otras palabras, use await en lugar de GetResult (Task.Result and Task.Wait should also be replaced with await).

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:

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

 399
Author: Stephen Cleary,
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().

 51
Author: Ykok,
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 .

 3
Author: Hasan Fathi,
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.

 0
Author: alex.peter,
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í:

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.

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.

 -1
Author: yamen,
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