Uma das principais diferenças está na propagação de exceções. Uma excepção, jogado dentro de um async Taskmétodo, é armazenado na devolvido Taskobjecto e permanece dormente até que a tarefa fica observada através de await task, task.Wait(), task.Resultou task.GetAwaiter().GetResult(). Ele é propagado dessa forma, mesmo se lançado da parte síncrona do asyncmétodo.
Considere o seguinte código, onde OneTestAsynce AnotherTestAsyncse comporta de maneira 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);
}
}
Se eu chamar DoTestAsync(OneTestAsync, -2), ele produzirá a seguinte saída:
Pressione Enter para continuar
Erro: um ou mais erros ocorreram.await Task.Delay
Erro: 2º
Note, eu tive que pressionar Enterpara ver.
Agora, se eu chamar DoTestAsync(AnotherTestAsync, -2), o fluxo de trabalho do código interno DoTestAsyncé bastante diferente, assim como a saída. Desta vez, não fui solicitado a pressionar Enter:
Erro: o valor precisa ser -1 (significando um tempo limite infinito), 0 ou um número inteiro positivo.
Nome do parâmetro: millisecondsDelayError: 1st
Em ambos os casos Task.Delay(-2) joga no início, ao validar seus parâmetros. Este pode ser um cenário inventado, mas em teoria Task.Delay(1000)pode lançar também, por exemplo, quando a API do cronômetro do sistema subjacente falha.
Em uma nota lateral, a lógica de propagação de erro ainda é diferente para async voidmétodos (em oposição aos async Taskmétodos). Uma exceção levantada dentro de um async voidmétodo será imediatamente relançada no contexto de sincronização do thread atual (via SynchronizationContext.Post), se o thread atual tiver um (SynchronizationContext.Current != null) . Caso contrário, ela será relançada via ThreadPool.QueueUserWorkItem). O chamador não tem chance de lidar com essa exceção no mesmo quadro de pilha.
Publiquei mais alguns detalhes sobre o comportamento de tratamento de exceções TPL aqui e aqui .
P : É possível simular o comportamento de propagação de exceções de asyncmétodos para métodos não-assíncronos Task, de modo que o último não atinja o mesmo frame de pilha?
R : Se realmente necessário, então sim, há um truque para isso:
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;
}
No entanto, observe que, sob certas condições (como quando é muito profundo na pilha), RunSynchronouslyainda pode ser executado de forma assíncrona.
Outra diferença notável é que
a versão async/ awaité mais propensa a travamento em um contexto de sincronização não padrão . Por exemplo, o seguinte irá travar em um aplicativo WinForms ou WPF:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait();
}
Altere para uma versão não assíncrona e não bloqueará:
Task TestAsync()
{
return Task.Delay(1000);
}
A natureza do bloqueio é bem explicada por Stephen Cleary em seu blog .
await/asyncem absoluto :)