Uma das principais diferenças está na propagação de exceções. Uma excepção, jogado dentro de um async Task
método, é armazenado na devolvido Task
objecto e permanece dormente até que a tarefa fica observada através de await task
, task.Wait()
, task.Result
ou task.GetAwaiter().GetResult()
. Ele é propagado dessa forma, mesmo se lançado da parte síncrona do async
método.
Considere o seguinte código, onde OneTestAsync
e AnotherTestAsync
se 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 void
métodos (em oposição aos async Task
métodos). Uma exceção levantada dentro de um async void
mé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 async
mé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), RunSynchronously
ainda 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
/async
em absoluto :)