Qual é o objetivo do "retorno aguardar" em c #?

251

Existe algum cenário em que escrever método como este:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

em vez disso:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

faria sentido?

Por que usar return awaitconstruct quando você pode retornar diretamente Task<T>da DoAnotherThingAsync()invocação interna ?

Eu vejo código return awaitem muitos lugares, acho que posso ter perdido alguma coisa. Mas, tanto quanto eu entendo, não usar palavras-chave async / wait neste caso e retornar diretamente a tarefa seria funcionalmente equivalente. Por que adicionar sobrecarga adicional de awaitcamada adicional ?

TX_
fonte
2
Acho que a única razão pela qual você vê isso é porque as pessoas aprendem por imitação e geralmente (se não precisam) usam a solução mais simples que podem encontrar. Então, as pessoas veem esse código, usam esse código, veem que funciona e, a partir de agora, para elas, é o caminho certo para fazê-lo ... Não há como esperar nesse caso
Fabio Marcolini,
7
Há pelo menos uma diferença importante: propagação de exceção .
noseratio
1
Também não entendo, não consigo compreender todo esse conceito, não faz nenhum sentido. Pelo que aprendi, se um método tem um tipo de retorno, DEVE ter uma palavra-chave de retorno, não são as regras da linguagem C #?
31418 monstro
@monstro a pergunta do OP tem a declaração de retorno?
David Klempfner 11/11/19

Respostas:

189

Há um caso sorrateiro quando returnno método normal e return awaitno asyncmétodo se comporta de maneira diferente: quando combinado com using(ou, mais geralmente, return awaitem um trybloco).

Considere estas duas versões de um método:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

O primeiro método fará Dispose()o Fooobjeto assim que o DoAnotherThingAsync()método retornar, o que provavelmente é muito antes de ele realmente ser concluído. Isso significa que a primeira versão provavelmente está com erros (porque Fooé descartada muito cedo), enquanto a segunda versão funcionará bem.

svick
fonte
4
Para completar, no primeiro caso, você deve retornarfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
Ghord
7
@ghord Isso não funcionaria, Dispose()retorna void. Você precisaria de algo parecido return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Mas não sei por que você faria isso quando pode usar a segunda opção.
svick 23/09/14
1
@ Rick Você está certo, deve ser mais ao longo das linhas de { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. O caso de uso é bastante simples: se você estiver no .NET 4.0 (como a maioria), ainda poderá escrever código assíncrono dessa maneira, que funcionará bem com os aplicativos 4.5.
Ghord
2
@ghord Se você está no .NET 4.0 e deseja escrever código assíncrono, provavelmente deve usar o Microsoft.Bcl.Async . E seu código é descartado Foosomente depois que o retorno é Taskconcluído, o que eu não gosto, porque introduz desnecessariamente a simultaneidade.
svick
1
@svick Seu código espera até que a tarefa seja concluída também. Além disso, o Microsoft.Bcl.Async não pode ser usado devido a dependência do KB2468871 e a conflitos ao usar a base de código assíncrona do .NET 4.0 com o código 4.5 assíncrono adequado.
Ghord
93

Se você não precisar async(por exemplo, você pode retornar o Taskdiretamente), não use async.

Há algumas situações em que return awaité útil, como se você tiver duas operações assíncronas para fazer:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

Para asyncsaber mais sobre desempenho, consulte o artigo e o vídeo de Stephen Toub no MSDN sobre o tópico.

Atualização: escrevi uma postagem de blog que entra em muito mais detalhes.

Stephen Cleary
fonte
13
Você poderia adicionar uma explicação sobre por que isso awaité útil no segundo caso? Por que não fazer return SecondAwait(intermediate);?
Matt Smith
2
Eu tenho a mesma pergunta que Matt, também não return SecondAwait(intermediate);alcançaria o objetivo? Eu acho que return awaité redundante aqui também ...
TX_
23
@ MattSmith Isso não compilaria. Se você deseja usar awaitna primeira linha, também deve usá-lo na segunda.
svick
2
@cateyes Não sei ao certo o que significa "sobrecarga introduzida paralelamente a eles", mas a asyncversão usará menos recursos (threads) que a versão síncrona.
svick
4
@ TomLint Realmente não compila. Supondo que o tipo de retorno de SecondAwaité `string, a mensagem de erro é:" CS4016: Como esse é um método assíncrono, a expressão de retorno deve ser do tipo 'string' em vez de 'Task <string>' ".
svick
23

O único motivo para você querer fazer isso é se houver algum outro awaitno código anterior ou se você estiver de alguma forma manipulando o resultado antes de devolvê-lo. Outra maneira pela qual isso pode estar acontecendo é através de uma try/catchalteração na maneira como as exceções são tratadas. Se você não está fazendo nada disso, está certo, não há razão para adicionar a sobrecarga de criar o método async.

Servy
fonte
4
Assim como na resposta de Stephen, não entendo por que return awaitseria necessário (em vez de apenas retornar a tarefa de invocação de filhos), mesmo que haja mais alguma espera no código anterior . Poderia, por favor, fornecer uma explicação?
TX_
10
@TX_ Se você deseja remover async, como aguardaria a primeira tarefa? Você precisa marcar o método como asyncse quisesse usar qualquer espera. Se o método estiver marcado como asynce você tiver um awaitcódigo anterior, precisará awaitda segunda operação assíncrona para que ele seja do tipo apropriado. Se você apenas removesse await, ele não seria compilado, pois o valor de retorno não seria do tipo apropriado. Desde que o método é asynco resultado é sempre envolvido em uma tarefa.
Servy
11
@Noseratio Experimente os dois. A primeira compila. O segundo não. A mensagem de erro informa o problema. Você não retornará o tipo adequado. Quando em um asyncmétodo você não retorna uma tarefa, você retorna o resultado da tarefa que será então quebrada.
Servy
3
@ Servy, é claro - você está certo. No último caso, retornaríamos Task<Type>explicitamente, enquanto asyncditamos a retornar Type(na qual o próprio compilador se transformaria Task<Type>).
noseratio
3
@ Ittsik Bem, com certeza, asyncé apenas um açúcar sintático para ligar explicitamente as continuações. Você não precisa async fazer nada, mas ao realizar praticamente qualquer operação assíncrona não trivial, é muito mais fácil trabalhar com isso. Por exemplo, o código que você forneceu não propaga erros da maneira que você deseja, e fazê-lo corretamente em situações ainda mais complexas começa a ficar muito mais difícil. Embora você nunca precise async , as situações que descrevo são onde está agregando valor para usá-lo.
Servy
17

Outro caso em que você pode precisar aguardar o resultado é este:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

Nesse caso, GetIFooAsync()deve aguardar o resultado de, GetFooAsyncporque o tipo de Té diferente entre os dois métodos e Task<Foo>não é diretamente atribuível a Task<IFoo>. Mas se você aguardar o resultado, ele se tornará o Fooque é diretamente atribuível IFoo. Em seguida, o método assíncrono apenas reembala o resultado por dentro Task<IFoo>e por fora.

Andrew Arnott
fonte
1
Concordo, isso é realmente irritante - acredito que a causa subjacente é que Task<>é invariável.
StuartLC 12/04/19
7

Tornar o método "thunk" simples de outra forma assíncrono cria uma máquina de estado assíncrona na memória, enquanto a não assíncrona não. Embora isso possa apontar as pessoas para o uso da versão não assíncrona porque é mais eficiente (o que é verdade), isso também significa que, no caso de um travamento, você não tem evidências de que esse método esteja envolvido na "pilha de retorno / continuação" o que às vezes torna mais difícil entender o jeito.

Então, sim, quando o perf não for crítico (e geralmente não é), lançarei o assíncrono em todos esses métodos de thunk, para que eu tenha a máquina de estado assíncrona para me ajudar a diagnosticar travamentos mais tarde e também para garantir que, se esses Se os métodos thunk evoluírem com o tempo, eles retornarão tarefas com falha em vez de jogar.

Andrew Arnott
fonte
6

Se você não usar o retorno aguardar, poderá arruinar o rastreamento da pilha durante a depuração ou quando ele for impresso nos logs com exceções.

Quando você retorna a tarefa, o método cumpriu sua finalidade e está fora da pilha de chamadas. Quando você usareturn await você o deixa na pilha de chamadas.

Por exemplo:

Pilha de chamadas ao usar a espera: A aguardando a tarefa de B => B aguardando a tarefa de C

Pilha de chamadas quando não estiver usando aguardar: A aguardando a tarefa de C, que B retornou.

subir
fonte
2

Isso também me confunde e sinto que as respostas anteriores ignoraram sua pergunta real:

Por que usar return wait aguardar construção quando você pode retornar diretamente Task da chamada interna DoAnotherThingAsync ()?

Bem, às vezes você realmente deseja um Task<SomeType>, mas na maioria das vezes deseja uma instância do SomeTyperesultado da tarefa.

Do seu código:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Uma pessoa não familiarizada com a sintaxe (eu, por exemplo) pode pensar que esse método deve retornar a Task<SomeResult>, mas como está marcado com async, significa que seu tipo de retorno real é SomeResult. Se você apenas usar return foo.DoAnotherThingAsync(), retornará uma tarefa que não será compilada. A maneira correta é retornar o resultado da tarefa, então o return await.

heltonbiker
fonte
1
"tipo de retorno real". Eh? assíncrono / espera não está mudando os tipos de retorno. No seu exemplo var task = DoSomethingAsync();, você daria uma tarefa, nãoT
Shoe
2
@ Sapato Não tenho certeza se entendi bem a async/awaitcoisa. Para meu entendimento Task task = DoSomethingAsync(), enquanto Something something = await DoSomethingAsync()ambos trabalham. O primeiro fornece a tarefa adequada, enquanto o segundo, devido à awaitpalavra - chave, fornece o resultado da tarefa após a conclusão. Eu poderia, por exemplo, ter Task task = DoSomethingAsync(); Something something = await task;.
precisa saber é o seguinte