Una diferencia importante está en la propagación de excepciones. Una excepción, arrojado dentro de un async Task
método, se almacena en el vuelto Task
objeto y permanece latente hasta que la tarea se observa a través de await task
, task.Wait()
, task.Result
o task.GetAwaiter().GetResult()
. Se propaga de esta manera incluso si se lanza desde la parte sincrónica del async
método.
Considere el siguiente código, donde OneTestAsync
y se AnotherTestAsync
comportan de manera bastante diferente:
static async Task OneTestAsync(int n)
{
await Task.Delay(n);
}
static Task AnotherTestAsync(int n)
{
return Task.Delay(n);
}
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
task = whatTest(n);
Console.Write("Press enter to continue");
Console.ReadLine();
task.Wait();
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
}
Si llamo DoTestAsync(OneTestAsync, -2)
, produce la siguiente salida:
Presione enter para continuar
Error: se produjeron uno o más errores. Espere Task.Delay
Error: segundo
Tenga en cuenta que tuve que presionar Enterpara verlo.
Ahora, si llamo DoTestAsync(AnotherTestAsync, -2)
, el flujo de trabajo del código interno DoTestAsync
es bastante diferente, al igual que la salida. Esta vez, no me pidieron que presione Enter:
Error: el valor debe ser -1 (que significa un tiempo de espera infinito), 0 o un número entero positivo.
Nombre del parámetro: milisegundosDelayError: 1st
En ambos casos se Task.Delay(-2)
lanza al principio, mientras valida sus parámetros. Este podría ser un escenario inventado, pero en teoría Task.Delay(1000)
también puede producirse , por ejemplo, cuando falla la API del temporizador del sistema subyacente.
En una nota al margen, la lógica de propagación de errores es aún diferente para los async void
métodos (a diferencia de los async Task
métodos). Una excepción generada dentro de un async void
método se volverá a lanzar inmediatamente en el contexto de sincronización del hilo actual (vía SynchronizationContext.Post
), si el hilo actual tiene uno ( SynchronizationContext.Current != null)
. De lo contrario, se volverá a lanzar vía ThreadPool.QueueUserWorkItem
). La persona que llama no tiene la oportunidad de manejar esta excepción en el mismo marco de pila.
Publiqué algunos detalles más sobre el comportamiento de manejo de excepciones de TPL aquí y aquí .
P : ¿Es posible imitar el comportamiento de propagación de excepciones de los async
métodos para métodos no Task
basados en asíncronos , de modo que este último no se ejecute en el mismo marco de pila?
R : Si realmente es necesario, entonces sí, hay un truco para eso:
async Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
}
Task<int> MethodAsync(int arg)
{
var task = new Task<int>(() =>
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
});
task.RunSynchronously(TaskScheduler.Default);
return task;
}
Sin embargo, tenga en cuenta que bajo ciertas condiciones (como cuando está demasiado profundo en la pila), RunSynchronously
aún podría ejecutarse de forma asincrónica.
Otra diferencia notable es que
la versión async
/ await
es más propensa a bloquearse en un contexto de sincronización no predeterminado . Por ejemplo, lo siguiente se bloqueará en una aplicación WinForms o WPF:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait();
}
Cámbielo a una versión no asíncrona y no se bloqueará:
Task TestAsync()
{
return Task.Delay(1000);
}
La naturaleza del bloqueo está bien explicada por Stephen Cleary en su blog .
await
/async
en absoluto :)