¿Se puede reemplazar la última espera con una espera explícita?
-
11-12-2019 - |
Pregunta
todavía estoy aprendiendo el async
/await
, así que discúlpeme si pregunto algo obvio.Considere el siguiente ejemplo:
class Program {
static void Main(string[] args) {
var result = FooAsync().Result;
Console.WriteLine(result);
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 22;
}
);
}
}
Esto se comporta como se esperaba e imprime "33" en la consola.
Si reemplazo el segundo await
con una espera explícita...
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = t2.Result;
return result1 + result2;
}
... Parece que tengo el mismo comportamiento.
¿Son estos dos ejemplos completamente equivalentes?
Y si son equivalentes en este caso, ¿existen otros casos en los que reemplazar el último await
¿Por una espera explícita haría la diferencia?
Solución 3
Bien, creo que ya me di cuenta de esto, así que permítanme resumirlo, en lo que espero sea una explicación más completa que las respuestas proporcionadas hasta ahora...
Respuesta corta
Reemplazo del segundo await
con una espera explícita no tendrá ningún efecto apreciable en una aplicación de consola, pero bloqueará el hilo de la interfaz de usuario de una aplicación WPF o WinForms durante la espera.
Además, el manejo de excepciones es ligeramente diferente (como señaló Stephen Cleary).
Respuesta larga
En pocas palabras, el await
Haz esto:
- Si la tarea esperada ya finalizó, simplemente recupera su resultado y continúa.
- Si no es así, publicaciones la continuación (el resto del método después del
await
) hacia actual contexto de sincronización, si lo hay.Esencialmente,await
está intentando devolvernos al punto de partida.- Si no hay un contexto actual, simplemente usa el original. Programador de tareas, que suele ser un grupo de subprocesos.
El segundo (y el tercero y así sucesivamente...) await
hace lo mismo.
Dado que las aplicaciones de consola normalmente no tienen contexto de sincronización, las continuaciones normalmente serán manejadas por el grupo de subprocesos, por lo que no hay problema si bloqueamos dentro de la continuación.
WinForms o WPF, por otro lado, tienen implementado el contexto de sincronización además de su bucle de mensajes.Por lo tanto, await
ejecutado en un subproceso de la interfaz de usuario (eventualmente) también ejecutará su continuación en el subproceso de la interfaz de usuario.Si bloqueamos la continuación, bloqueará el bucle de mensajes y hará que la interfaz de usuario no responda hasta que la desbloqueemos.OTOH, si tan solo await
, publicará claramente las continuaciones que eventualmente se ejecutarán en el hilo de la interfaz de usuario, sin bloquear nunca el hilo de la interfaz de usuario.
En el siguiente formulario de WinForms, que contiene un botón y una etiqueta, usando await
mantiene la interfaz de usuario receptiva en todo momento (tenga en cuenta la async
delante del controlador de clic):
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private async void button1_Click(object sender, EventArgs e) {
var result = await FooAsync();
label1.Text = result.ToString();
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(3000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(5000);
return 22;
}
);
}
}
Si reemplazamos el segundo await
en FooAsync
con t2.Result
, continuaría respondiendo durante aproximadamente 3 segundos después de hacer clic en el botón y luego se congelaría durante aproximadamente 2 segundos:
- La continuación después de la primera.
await
esperará cortésmente su turno para ser programado en el hilo de la interfaz de usuario, lo que sucedería despuésMethod1Async()
la tarea finaliza, es decirdespués de unos 3 segundos, - en cuyo punto el
t2.Result
bloqueará bruscamente el hilo de la interfaz de usuario hasta queMethod2Async()
La tarea finaliza, aproximadamente 2 segundos después.
Si eliminamos el async
en frente de button1_Click
y reemplazó su await
con FooAsync().Result
se estancaría:
- El hilo de la interfaz de usuario esperaría
FooAsync()
tarea por terminar, - que esperaría a que terminara su continuación,
- que esperaría a que el hilo de la interfaz de usuario esté disponible,
- lo cual no lo es, ya que está bloqueado por el
FooAsync().Result
.
El artículo "Await, SynchronizationContext y aplicaciones de consola" de Stephen Toub fue invaluable para mí a la hora de comprender este tema.
Otros consejos
Su versión de reemplazo bloquea el hilo de llamada esperando a que finalice la tarea.Es difícil ver una diferencia visible en una aplicación de consola como esa, ya que estás bloqueando intencionalmente en Principal, pero definitivamente no son equivalentes.
No son equivalentes.
Task.Result
bloques hasta que el resultado esté disponible.Como explico en mi blog, esto puede causar puntos muertos si tienes un async
contexto que requiere acceso exclusivo (por ejemplo, una interfaz de usuario o una aplicación ASP.NET).
También, Task.Result
envolverá cualquier excepción en AggregateException
, por lo que el manejo de errores es más difícil si bloquea sincrónicamente.