espera vs Tarea. Espera - ¿Punto muerto?

194

No entiendo la diferencia entre Task.Waity await.

Tengo algo similar a las siguientes funciones en un servicio ASP.NET WebAPI:

public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

¿Dónde Getquedará estancado?

Que podria causar esto? ¿Por qué esto no causa un problema cuando uso una espera de bloqueo en lugar de hacerlo await Task.Delay?

ronag
fuente
@Servy: Volveré con un repositorio tan pronto como tenga tiempo. Por ahora funciona con lo Task.Delay(1).Wait()que es lo suficientemente bueno.
Ronag
2
Task.Delay(1).Wait()es básicamente exactamente lo mismo que Thread.Sleep(1000). En el código de producción real rara vez es apropiado.
Servy
@ronag: Tu WaitAllestá causando el punto muerto. Vea el enlace a mi blog en mi respuesta para más detalles. Deberías usar await Task.WhenAllen su lugar.
Stephen Cleary
66
@ronag Debido a que tiene ConfigureAwait(false)una sola llamada a un punto muerto Baro Rosno lo hará, pero debido a que tiene un número enumerable que está creando más de uno y luego espera a todos, la primera barra bloqueará el segundo. Si en await Task.WhenAlllugar de esperar todas las tareas, para no bloquear el contexto ASP, verá que el método regresa normalmente.
Servy
2
@ronag Su otra opción sería añadir el .ConfigureAwait(false) todo el camino hasta el árbol hasta que se bloquea, de esa manera nada está siempre tratando de volver al contexto principal; eso funcionaria. Otra opción sería activar un contexto de sincronización interna. Enlace . Si lo coloca Task.WhenAlldentro AsyncPump.Run, bloqueará efectivamente todo el proceso sin que lo necesite en ConfigureAwaitningún lado, pero esa es probablemente una solución demasiado compleja.
Servy

Respuestas:

268

Waity await, aunque son conceptualmente similares, en realidad son completamente diferentes.

Waitse bloqueará sincrónicamente hasta que se complete la tarea. Por lo tanto, el hilo actual está literalmente bloqueado esperando que se complete la tarea. Como regla general, debe usar " asynctodo el camino hacia abajo"; es decir, no bloquee el asynccódigo. En mi blog, entro en los detalles de cómo el bloqueo en código asincrónico causa un punto muerto .

awaitesperará asincrónicamente hasta que se complete la tarea. Esto significa que el método actual está "en pausa" (se captura su estado) y el método devuelve una tarea incompleta a la persona que llama. Más tarde, cuando se awaitcompleta la expresión, el resto del método se programa como una continuación.

También mencionó un "bloque cooperativo", por el cual supongo que se refiere a una tarea que está Waitejecutando en el hilo de espera. Hay situaciones en las que esto puede suceder, pero es una optimización. Hay muchas situaciones en las que no puede suceder, como si la tarea es para otro planificador, o si ya se inició o si es una tarea sin código (como en su ejemplo de código: Waitno puede ejecutar la Delaytarea en línea porque no hay código para ello).

Puede encontrar mi async/ awaitintroducción útil.

Stephen Cleary
fuente
44
Creo que hay un malentendido, Waitfunciona bien awaitinterbloqueos.
Ronag
55
No, el programador de tareas no hará eso. Waitbloquea el hilo y no se puede usar para otras cosas.
Stephen Cleary
8
@ronag Supongo que acabas de mezclar los nombres de tus métodos y tu bloqueo se debió al código de bloqueo y funcionó con el awaitcódigo. O eso, o el punto muerto no estaba relacionado con ninguno de los dos y diagnosticaste mal el problema.
Servy
3
@hexterminator: Esto es por diseño: funciona muy bien para aplicaciones de interfaz de usuario, pero tiende a interferir con las aplicaciones ASP.NET. ASP.NET Core ha solucionado esto eliminando el SynchronizationContext, por lo que el bloqueo dentro de una solicitud de ASP.NET Core ya no se bloquea.
Stephen Cleary
1
en msdn aquí, dice que la espera se ejecuta en un hilo separado de forma asincrónica. ¿Me estoy perdiendo de algo? msdn.microsoft.com/en-us/library/hh195051(v=vs.110).aspx
batmaci
6

Basado en lo que leí de diferentes fuentes:

Una awaitexpresión no bloquea el hilo en el que se está ejecutando. En cambio, hace que el compilador registre el resto del asyncmétodo como una continuación de la tarea esperada. El control luego vuelve a la persona que llama del asyncmétodo. Cuando se completa la tarea, invoca su continuación y la ejecución deasync método se reanuda donde lo dejó.

Para esperar taska que se complete un solo , puede llamar a su Task.Waitmétodo. Una llamada al Waitmétodo bloquea el hilo de llamada hasta que la instancia de clase única haya completado la ejecución. El Wait()método sin parámetros se utiliza para esperar incondicionalmente hasta que se complete una tarea. La tarea simula el trabajo llamando al Thread.Sleepmétodo para dormir durante dos segundos.

Este artículo también es una buena lectura.

Ayushmati
fuente
3
"¿No es eso técnicamente incorrecto? ¿Alguien puede aclarar?" - ¿Puedo aclararlo? ¿Lo preguntas como una pregunta? (Solo quiero aclarar si estás preguntando o respondiendo). Si está preguntando: puede funcionar mejor como una pregunta separada; es improbable reunir nuevas respuestas aquí como respuesta
Marc Gravell
1
Respondí la pregunta y formulé una pregunta por separado por la duda que tenía aquí stackoverflow.com/questions/53654006/… Gracias @MarcGravell. ¿Puede por favor eliminar su voto de eliminación para la respuesta ahora?
Ayushmati
"¿Puedes por favor eliminar tu voto de borrado para la respuesta ahora?" - eso no es mío; gracias a ♦, cualquier voto de mi parte hubiera tenido efecto de inmediato. Sin embargo, no creo que esto responda a los puntos clave de la pregunta, que trata sobre el comportamiento de punto muerto.
Marc Gravell
-2

Algunos hechos importantes no se dieron en otras respuestas:

La "espera asíncrona" es más compleja a nivel de CIL y, por lo tanto, cuesta memoria y tiempo de CPU.

Cualquier tarea puede cancelarse si el tiempo de espera es inaceptable.

En el caso de que "async aguarde" no tenemos un controlador para dicha tarea para cancelarlo o monitorearlo.

Usar Task es más flexible que "async aguarda".

Cualquier funcionalidad de sincronización puede ser envuelta por async.

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 

No veo por qué debo vivir con la duplicación de código para el método de sincronización y asíncrono o el uso de hacks.

usuario1785960
fuente