Por que você 'aguardaria' um método e depois interrogaria imediatamente seu valor de retorno?

24

Em este artigo MSDN , o código seguinte exemplo é fornecido (ligeiramente editado por brevidade):

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Department department = await db.Departments.FindAsync(id);

    if (department == null)
    {
        return HttpNotFound();
    }

    return View(department);
}

O FindAsyncmétodo recupera um Departmentobjeto por seu ID e retorna a Task<Department>. Em seguida, o departamento é imediatamente verificado para ver se é nulo. Pelo que entendi, solicitar o valor da tarefa dessa maneira bloqueará a execução do código até que o valor do método esperado seja retornado, efetivamente fazendo desta uma chamada síncrona.

Por que você faria isso? Não seria mais simples chamar o método síncrono Find(id), se você vai bloquear imediatamente?

Robert Harvey
fonte
Pode estar relacionado à implementação. ... else return null;Em seguida, você precisa verificar se o método realmente encontrou o departamento solicitado.
Jeremy Kato
Não vejo qualquer em um asp.net, mas em um aplicativo destop, por fazê-lo desta forma você não está congelando a ui
Rémi
Aqui está um link que explicar o conceito aguardam do Designer ... msdn.microsoft.com/en-us/magazine/hh456401.aspx
Jon Raynor
Vale a pena esperar apenas no ASP.NET se as opções de contato de encadeamento estiverem diminuindo sua velocidade, ou se o uso de memória de muitas pilhas de encadeamentos for um problema para você.
Ian

Respostas:

24

Pelo que entendi, solicitar o valor da tarefa dessa maneira bloqueará a execução do código até que o valor do método esperado seja retornado, efetivamente fazendo desta uma chamada síncrona.

Não é bem assim.

Quando você chama, await db.Departments.FindAsync(id)a tarefa é enviada e o encadeamento atual é retornado ao pool para uso por outras operações. O fluxo de execução é bloqueado (como seria independentemente do uso departmentlogo após, se eu entendi as coisas corretamente), mas o encadeamento em si é livre para ser usado por outras coisas enquanto você espera pela conclusão da operação fora da máquina (e sinalizado por uma porta de evento ou conclusão).

Se você ligou d.Departments.Find(id), o encadeamento fica lá e aguarda a resposta, mesmo que a maior parte do processamento esteja sendo feita no banco de dados.

Você está efetivamente liberando recursos da CPU quando vinculado ao disco.

Telastyn
fonte
2
Mas eu pensei que tudo o que awaitfiz foi assinar o restante do método como uma continuação no mesmo thread (há exceções; alguns métodos assíncronos ativam seu próprio thread) ou assinar o asyncmétodo como uma continuação no mesmo thread e permitir que o código restante a ser executado (como você pode ver, não sou muito claro como asyncfunciona). O que você está descrevendo soa mais como uma forma sofisticada deThread.Sleep(untilReturnValueAvailable)
Robert Harvey
11
@RobertHarvey - ele a atribui como uma continuação, mas depois que você envia a tarefa (e sua continuação) para ser processada, não resta mais nada para executar. Não é garantido que esteja no mesmo encadeamento, a menos que você o especifique (via ConfigureAwaitiirc).
Telastyn 11/08/16
11
Veja minha resposta ... a continuação é empacotada de volta ao thread original por padrão.
Michael Brown
2
Eu acho que vejo o que estou perdendo aqui. Para que isso proporcione algum benefício, a estrutura do ASP.NET MVC deve awaitser chamada public async Task<ActionResult> Details(int? id). Caso contrário, a chamada original será bloqueada, aguardando department == nulla resolução.
Robert Harvey
2
@RobertHarvey Quando await ..."retorna", a FindAsyncchamada termina. Isso é o que a espera faz. É chamado de aguardar porque faz com que seu código aguarde as coisas. (Mas note que não é o mesmo que fazer a espera thread atual para coisas)
user253751
17

Eu realmente odeio que nenhum dos exemplos mostre como é possível esperar algumas linhas antes de aguardar a tarefa. Considere isto.

Foo foo = await getFoo();
Bar bar = await getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(foo, bar);

Esse é o tipo de código que os exemplos incentivam, e você está certo. Há pouco sentido nisso. Ele libera o thread principal para fazer outras coisas, como responder à entrada da interface do usuário, mas o verdadeiro poder do async / waitit é que eu posso continuar fazendo outras coisas facilmente enquanto aguardo a conclusão de uma tarefa potencialmente demorada. O código acima "bloqueará" e espere para executar a linha de impressão até obtermos o Foo & Bar. Não há necessidade de esperar embora. Podemos processar isso enquanto esperamos.

Task<Foo> foo = getFoo();
Task<Bar> bar = getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(await foo, await bar);

Agora, com o código reescrito, não paramos e aguardamos nossos valores até que seja necessário. Estou sempre atento a esse tipo de oportunidade. Ser esperto quando aguardamos pode levar a melhorias significativas no desempenho. Hoje em dia, temos vários núcleos, e podemos usá-los.

Pato de borracha
fonte
11
Hum, isso é menos sobre núcleos / threads e mais sobre como usar chamadas assíncronas corretamente.
Deduplicator
Você não está errado @Deduplicator. É por isso que citei "bloquear" na minha resposta. É difícil falar com essas coisas sem mencionar tópicos, mas, embora reconheça que pode ou não haver vários threads envolvidos.
RubberDuck
Eu sugiro que você tenha cuidado ao aguardar dentro de outra chamada de método. Às vezes, o compilador entende mal o que está aguardando e você recebe uma exceção realmente estranha. Depois que tudo assíncrono / espera é apenas açúcar de sintaxe, você pode verificar com sharplab.io a aparência do código gerado. Eu observei isso em várias ocasiões e agora apenas espero uma linha acima da chamada onde o resultado é necessário ... não preciso dessas dores de cabeça.
evictednoise 5/03
"Há pouco sentido nisso." - Há muito sentido nisso. Os casos em que "continuar fazendo outras coisas" são aplicáveis ​​no mesmo método não são tão comuns; é muito mais comum que você queira awaite deixe o thread fazer coisas completamente diferentes.
Sebastian Redl
Quando eu disse “há pouco sentido nisso” @SebastianRedl, eu quis dizer o caso em que você obviamente poderia executar as duas tarefas em paralelo em vez de executar, esperar, executar, esperar. Isso é muito mais comum do que você pensa que é. Aposto que se você olhasse em torno de sua base de código, encontraria oportunidades.
RubberDuck
6

Então, há mais o que acontece nos bastidores aqui. Async / Await é açúcar sintático. Primeiro, observe a assinatura da função FindAsync. Retorna uma tarefa. Já está vendo a mágica da palavra-chave, ela descompacta essa tarefa em um departamento.

A função de chamada não é bloqueada. O que acontece é que a atribuição ao departamento e tudo o que segue a palavra-chave wait são encerradas e, para todos os efeitos, são passadas para o método Task.ContinueWith (a função FindAsync é executada automaticamente em um thread diferente).

É claro que há mais o que acontece nos bastidores, porque a operação é reorganizada no encadeamento original (para que você não precise mais se preocupar em sincronizar com a interface do usuário ao executar uma operação em segundo plano) e no caso de a função de chamada ser Async ( e sendo chamado de forma assíncrona) a mesma coisa acontece na pilha.

Então o que acontece é que você adquire a magia das operações assíncronas, sem as armadilhas.

Michael Brown
fonte
1

Não, não está retornando imediatamente. A espera torna o método chamado assíncrono. Quando FindAsync é chamado, o método Details retorna com uma tarefa que não está concluída. Quando o FindAsync terminar, ele retornará seu resultado na variável de departamento e retomará o restante do método Detalhes.

Steve
fonte
Não, não há nenhum bloqueio. Ele nunca chegará ao departamento == null até que o método assíncrono esteja concluído.
Steve
11
async awaitgeralmente não cria novos encadeamentos e, mesmo que tenha criado, você ainda precisa aguardar que o valor do departamento descubra se é nulo.
Robert Harvey
2
Então, basicamente, o que você está dizendo é que, para que isso seja de algum benefício, a chamada para public async Task<ActionResult> também deve ser awaitatendida.
Robert Harvey
2
@RobertHarvey Sim, você entendeu. Async / Await é essencialmente viral. Se você "aguarda" uma função, também deve aguardar as funções que a chamam. awaitnão deve ser misturado com .Wait()ou .Result, pois isso pode causar conflitos. A cadeia async / waitit geralmente termina em uma função com uma async voidassinatura, usada principalmente para manipuladores de eventos ou para funções invocadas diretamente pelos elementos da interface do usuário.
KChaloux
11
@ Steve - " ... portanto, seria em seu próprio thread. " Não, leia Tarefas ainda não são threads e assíncrono não é paralelo . Isso inclui a saída real demonstrando que um novo thread não foi criado.
Home
1

Eu gosto de pensar em "assíncrono" como um contrato, um contrato que diz "eu posso executar isso de forma assíncrona se você precisar, mas você pode me chamar como qualquer outra função síncrona".

Ou seja, um desenvolvedor estava fazendo funções e algumas decisões de design os levaram a fazer / marcar várias funções como "assíncronas". O chamador / consumidor das funções tem a liberdade de usá-las como quiserem. Como você diz, você pode chamar a chamada em espera imediatamente antes da chamada da função e aguardá-la. Dessa forma, você a tratou como uma função síncrona, mas se você quiser, pode chamá-la sem aguardar como

Task<Department> deptTask = db.Departments.FindAsync(id);

e depois, digamos, 10 linhas abaixo da função que você chama

Department d = await deptTask;

portanto, tratando-o como uma função assíncrona.

Você decide.

user734028
fonte
11
É mais como "Eu me reservo o direito de manipular a mensagem de forma assíncrona, use isso para obter o resultado".
Deduplicator
-1

"se você vai bloquear imediatamente de qualquer maneira" a resposta é "Sim". Somente quando você precisar de uma resposta rápida, aguardar / assíncrono fará sentido. Por exemplo, o encadeamento da interface do usuário chega a um método realmente assíncrono, o encadeamento da interface do usuário retornará e continuará a ouvir o clique do botão, enquanto o código abaixo de "aguardar" será excutado por outro encadeamento e, finalmente, obterá o resultado.

user10200952
fonte
11
O que essa resposta fornece que as outras respostas não?