'esperar' funciona, pero llama a la tarea. El resultado se bloquea

126

Tengo las siguientes cuatro pruebas y la última se cuelga cuando la ejecuto. Por qué pasó esto:

[Test]
public void CheckOnceResultTest()
{
    Assert.IsTrue(CheckStatus().Result);
}

[Test]
public async void CheckOnceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceResultTest()
{
    Assert.IsTrue(CheckStatus().Result); // This hangs
    Assert.IsTrue(await CheckStatus());
}

private async Task<bool> CheckStatus()
{
    var restClient = new RestClient(@"https://api.test.nordnet.se/next/1");
    Task<IRestResponse<DummyServiceStatus>> restResponse = restClient.ExecuteTaskAsync<DummyServiceStatus>(new RestRequest(Method.GET));
    IRestResponse<DummyServiceStatus> response = await restResponse;
    return response.Data.SystemRunning;
}

Utilizo este método de extensión para restsharp RestClient :

public static class RestClientExt
{
    public static Task<IRestResponse<T>> ExecuteTaskAsync<T>(this RestClient client, IRestRequest request) where T : new()
    {
        var tcs = new TaskCompletionSource<IRestResponse<T>>();
        RestRequestAsyncHandle asyncHandle = client.ExecuteAsync<T>(request, tcs.SetResult);
        return tcs.Task;
    }
}
public class DummyServiceStatus
{
    public string Message { get; set; }
    public bool ValidVersion { get; set; }
    public bool SystemRunning { get; set; }
    public bool SkipPhrase { get; set; }
    public long Timestamp { get; set; }
}

¿Por qué se cuelga la última prueba?

Johan Larsson
fuente
77
Debe evitar devolver el vacío de los métodos asincrónicos. Es solo para la compatibilidad con los controladores de eventos existentes, principalmente en el código de interfaz. Si su método asíncrono no devuelve nada, debería devolver Tarea. Tuve numerosos problemas con MSTest y anulé las pruebas asíncronas de retorno.
ghord
2
@ghord: MSTest no admite async voidmétodos de prueba de unidad en absoluto; simplemente no funcionarán. Sin embargo, NUnit sí. Dicho esto, estoy de acuerdo con el principio general de preferir async Tasksobre async void.
Stephen Cleary
@StephenCleary Sí, aunque estaba permitido en versiones beta de VS2012, lo que estaba causando todo tipo de problemas.
ghord

Respuestas:

88

Se encuentra con la situación de bloqueo estándar que describo en mi blog y en un artículo de MSDN : el asyncmétodo está intentando programar su continuación en un hilo que está bloqueado por la llamada a Result.

En este caso, SynchronizationContextNUnit utiliza el suyo para ejecutar async voidmétodos de prueba. Intentaría usar async Taskmétodos de prueba en su lugar.

Stephen Cleary
fuente
44
cambiando a asíncrono La tarea funcionó, ahora necesito leer el contenido de sus enlaces un par de veces, señor.
Johan Larsson
@ MarioLopez: La solución es usar " asynctodo el camino" (como se señala en mi artículo de MSDN). En otras palabras, como dice el título de mi blog, "no bloquee el código asíncrono".
Stephen Cleary
1
@StephenCleary ¿y si tengo que llamar a un método asíncrono dentro de un constructor? Los constructores no pueden ser asíncronos.
Raikol Amaro
1
@StephenCleary En casi todas sus respuestas sobre SO y en sus artículos, todo lo que veo de lo que habla es reemplazar Wait()con hacer el método de llamada async. Pero para mí, esto parece estar empujando el problema hacia arriba. En algún momento, algo tiene que ser administrado sincrónicamente. ¿Qué sucede si mi función es intencionadamente sincrónica porque administra hilos de trabajo de larga ejecución Task.Run()? ¿Cómo espero a que termine sin bloquearse dentro de mi prueba NUnit?
void.pointer
1
@ void.pointer: At some point, something has to be managed synchronously.- en absoluto. Para aplicaciones de IU, el punto de entrada puede ser un async voidcontrolador de eventos. Para aplicaciones de servidor, el punto de entrada puede ser una async Task<T>acción. Es preferible usar asyncambos para evitar el bloqueo de hilos. Puede hacer que su prueba de NUnit sea síncrona o asíncrona; si es asíncrono, hágalo en async Tasklugar de async void. Si es sincrónico, no debería tener un, SynchronizationContextasí que no debería haber un punto muerto.
Stephen Cleary
222

Adquisición de un valor a través de un método asíncrono:

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

Llamar sincrónicamente un método asíncrono

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

No se producirán problemas de punto muerto debido al uso de Task.Run.

Herman Schoenfeld
fuente
15
-1 por fomentar el uso de async voidmétodos de prueba unitaria y eliminar las garantías del mismo hilo proporcionadas por SynchronizationContextel sistema bajo prueba.
Stephen Cleary
68
@StephenCleary: no hay "enouraging" de vacío asíncrono. Simplemente está empleando una construcción de C # válida para resolver el problema de punto muerto. El fragmento anterior es una solución indispensable y simple para el problema del OP. Stackoverflow se trata de soluciones a problemas, no de autopromoción detallada.
Herman Schoenfeld
81
@StephenCleary: sus artículos realmente no articulan la solución (al menos no claramente) e incluso si tuviera una solución, usaría tales construcciones indirectamente. Mi solución no utiliza contextos explícitamente, ¿y qué? El punto es que el mío funciona y es de una sola línea. No necesité dos publicaciones de blog y miles de palabras para resolver el problema. NOTA: Ni siquiera uso el vacío asíncrono , por lo que no sé realmente de qué se trata. ¿Ve "vacío asíncrono" en alguna parte de mi respuesta concisa y adecuada?
Herman Schoenfeld
15
@HermanSchoenfeld, si agregaste el por qué al cómo , creo que tu respuesta se beneficiaría mucho.
ironstone13
19
Sé que esto es un poco tarde, pero deberías usarlo en .GetAwaiter().GetResult()lugar de hacerlo .Resultpara que ninguno quede Exceptionenvuelto.
Camilo Terevinto
15

Puede evitar el punto muerto que se agrega ConfigureAwait(false)a esta línea:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

IRestResponse<DummyServiceStatus> response = await restResponse.ConfigureAwait(false);

Describí este escollo en la publicación de mi blog Escollos de async / await

Vladimir
fuente
9

Está bloqueando la IU utilizando la propiedad Task.Result. En la documentación de MSDN , han mencionado claramente que,

"La propiedad Resultado es una propiedad de bloqueo. Si intenta acceder a ella antes de que finalice su tarea, el subproceso que está actualmente activo se bloquea hasta que se complete la tarea y el valor esté disponible. En la mayoría de los casos, debe acceder al valor utilizando Esperar o esperar en lugar de acceder a la propiedad directamente ".

La mejor solución para este escenario sería eliminar los modos de espera y asíncrono de los métodos y usar solo la Tarea donde devuelve el resultado. No alterará su secuencia de ejecución.

Caballero oscuro
fuente
3

Si no recibe ninguna devolución de llamada o el control se cuelga, después de llamar a la función asíncrona de servicio / API, debe configurar Context para devolver un resultado en el mismo contexto llamado.

Utilizar TestAsync().ConfigureAwait(continueOnCapturedContext: false);

Se enfrentará a este problema solo en aplicaciones web, pero no en static void main.

Mayank Pandit
fuente
ConfigureAwaitevita el interbloqueo en ciertos escenarios al no ejecutarse en el contexto del hilo original
davidcarr