Espera por uma tarefa concluída igual a task.Result?

117

No momento, estou lendo " Concurrency in C # Cookbook ", de Stephen Cleary, e percebi a seguinte técnica:

var completedTask = await Task.WhenAny(downloadTask, timeoutTask);  
if (completedTask == timeoutTask)  
  return null;  
return await downloadTask;  

downloadTaské uma chamada para httpclient.GetStringAsynce timeoutTaskestá em execução Task.Delay.

Caso não tenha expirado, então downloadTaskjá está concluído. Por que é necessário fazer uma segunda espera em vez de retornar downloadTask.Result, visto que a tarefa já está concluída?

julio.g
fonte
3
Falta um pouco de contexto aqui e, a menos que as pessoas tenham acesso imediato ao livro, você precisará incluí-lo. O que é downloadTaske timeoutTask? O que eles fazem?
Mike Perrenoud
7
Não estou vendo uma verificação real de conclusão bem-sucedida aqui. A tarefa pode muito bem apresentar falhas e, nesse caso, o comportamento será diferente ( AggregateExceptioncom Resultvs primeira exceção via ExceptionDispatchInfocom await). Discutido com mais detalhes em "Manipulação de exceções de tarefas no .NET 4.5" de Stephen Toub: blogs.msdn.com/b/pfxteam/archive/2011/09/28/… )
Kirill Shlenskiy
você deve fazer disso uma resposta @KirillShlenskiy
Carsten
@MichaelPerrenoud Você está certo, obrigado por notar, vou editar a pergunta.
julio.g

Respostas:

160

Já existem algumas boas respostas / comentários aqui, mas apenas para interromper ...

Há duas razões pelas quais eu prefiro awaitmais Result(ou Wait). A primeira é que o tratamento de erros é diferente; awaitnão envolve a exceção em um AggregateException. Idealmente, o código assíncrono nunca deveria ter que lidar AggregateException, a menos que especificamente queira .

A segunda razão é um pouco mais sutil. Conforme descrevo em meu blog (e no livro), Result/ Waitpode causar deadlocks e pode causar ainda mais deadlocks sutis quando usado em um asyncmétodo . Portanto, quando estou lendo o código e vejo um Resultou Wait, é um sinalizador de aviso imediato. O Result/ Waitsó está correto se você tiver certeza absoluta de que a tarefa já foi concluída. Não é apenas difícil de ver à primeira vista (no código do mundo real), mas também é mais frágil para alterações de código.

Isso não quer dizer que Result/ nuncaWait deva ser usado. Eu sigo essas diretrizes em meu próprio código:

  1. O código assíncrono em um aplicativo só pode ser usado await.
  2. O código de utilitário assíncrono (em uma biblioteca) pode ocasionalmente usar Result/ Waitse o código realmente exigir. Esse uso provavelmente deve ter comentários.
  3. O código de tarefa paralela pode usar Resulte Wait.

Observe que (1) é de longe o caso comum, daí minha tendência de usar em awaittodos os lugares e tratar os outros casos como exceções à regra geral.

Stephen Cleary
fonte
Encontramos o deadlock usando 'result' em vez de 'await' em nossos projetos. a parte complicada é não ter erro de compilação e seu código fica instável depois de um tempo.
Ahmad Mousavi
@Stephen, você poderia me explicar por que "Idealmente, o código assíncrono nunca deveria ter que lidar com AggregateException, a menos que queira especificamente"
vcRobe
4
@vcRobe Porque awaitimpede o AggregateExceptioninvólucro. AggregateExceptionfoi projetado para programação paralela, não para programação assíncrona.
Stephen Cleary de
2
> "A espera está correta apenas se você tiver certeza absoluta de que a tarefa já foi concluída." .... Então por que é chamado Wait?
Ryan The Leach
4
@RyanTheLeach: O objetivo original de Waitera juntar a instâncias de Paralelismo de Tarefa Dinâmica Task . Usá-lo para esperar por Taskinstâncias assíncronas é perigoso. A Microsoft considerou a introdução de um novo tipo de "Promessa", mas optou por usar o existente Task; a desvantagem de reutilizar o Tasktipo existente para tarefas assíncronas é que você acaba com várias APIs que simplesmente não deveriam ser usadas em código assíncrono.
Stephen Cleary
12

Isso faz sentido se timeoutTaskfor um produto do Task.Delayqual acredito no que está no livro.

Task.WhenAnyretorna Task<Task>, onde a tarefa interna é uma daquelas que você passou como argumentos. Ele poderia ser reescrito assim:

Task<Task> anyTask = Task.WhenAny(downloadTask, timeoutTask);
await anyTask;
if (anyTask.Result == timeoutTask)  
  return null;  
return downloadTask.Result; 

Em ambos os casos, porque downloadTaskjá foi concluído, há uma diferença muito pequena entre return await downloadTaske return downloadTask.Result. É que o último irá lançar o AggregateExceptionque envolve qualquer exceção original, como apontado por @KirillShlenskiy nos comentários. O primeiro iria apenas relançar a exceção original.

Em qualquer um dos casos, sempre que você lida com exceções, deve verificar AggregateExceptionsuas exceções internas de qualquer maneira, para chegar à causa do erro.

noseratio
fonte