Vantagens do async/await (C#)

hferreira.80

Power Member
Boa tarde,

Tenho um backend em C# composto maioritariamente por procedimentos remotos (RPC). Estou a falar em cerca de 1 milhar de RPC's e alguns procedimentos REST (WebApi).

Não estava a tirar partido de async/await e decidi passar a usar, o qual obrigou a um refactoring enorme.
Infelizmente só se consegue testar depois do refactoring ter sido concluído.

Para minha desilução, a performance apercebida com um único utilizador é exatamente a mesma com e sem o async/await.

Bem sei, que não estou ainda a tirar partido de programação paralela (o qual agora poderei fazer muito mais facilmente com async/await/Task), pois isso irá obrigar a uma restruturação da lógica, que ficará para uma segunda fase em futuros procedimentos.

Mas como invoco vários procedimentos remotos ao mesmo tempo, imaginei que se sentiria uma diferença na performance, porque o ASP.NET conseguiria responder a mais pedidos em simultâneo.
Acredito que possa pelo menos melhorar a performance no servidor em multi-utilizador, que ainda não testei.
 
As operacoes que mais beneficiam com async/await sao todas aquelas que bloqueiam a thread e deixam o CPU em idle, como I/O.
O exemplo mais comum de aplicacao de tasks com async/await é quando o utilizador clica num botao de um UI, a logica e delegada e faz que a thread do UI nao bloqueie.
Se os teus metodos fizerem muitos accessos a ficheiros, outros webservices, bases de dados e afins, faz sentido utilizar async/await, ja que a thread nao vai bloquear durante o I/O e vai continuar com a execucao ate o resultado da operacao assincrona estar disponivel.
Se por outro lado as tuas operacoes forem muito intensivas em termos de processamento, vais ter menos performance utilizando async/await do que utilizando uma thread diferente, porque tens de adicionar ao tempo de computacao do metodo, o tempo de computacao de todo o sistema do async/await.
Tens um artigo na MSDN que explica isto com mais detalhe (e bem melhor do que eu alguma vez seria capaz) a clarificar o funcionamento do async/await e as suas alternativas.
 
Os meus métodos consomem poucos webservices mas são casos mais pontuais, ou seja, só por ai não se justificava no entanto efetuam imensos acessos a bases de dados (basicamente todo o tempo).

Parti do principio que se o método suporta a versão Async, iria usar.
A partir dai sou forçado a usar await, por sua vez a marcar o método como async e por fim a alterar o retorno para Task, ou seja, bastou deixar o compilador se queixar e ir corrigindo de acordo com a necessidade.

Sim, estou ciente que await/async não vem de graça mas parti do principio que o acesso à base de dados também não e é mais lento e como são webservices para serem consumidos por muitos utilizadores, teria algum proveito, pois permite suspender o processo até que o recurso externo termine (neste caso base de dados retorne com o resultado do seu processamento) o qual ainda não pude testar esta última questão.

Enquanto UI, só no arranque consume cerca de 20 e tal webservices em simultâneo e por isso esperava ver alguma diferença.
A performance é exatamente a mesma mas creio que esteja identificado.
Cada serviço é extretamentente rápido, não sendo possível identificar efetivamente a suspensão de um com maior processamento em paralelo de outros.

Onde sei (ou julgo que) irei tirar mesmo grande proveito, será com paralelismo, o qual tomei conhecimento mas decidi não fazer ainda, pois isto é uma versão alterada para async mas sem grande modificações da lógica e mesmo assim já estamos a falar de um refactoring gigantesco, enquanto que com paralelismo teria de modificar a lógica e analisar quais as operações onde poderei aplicar.
Irei usar em novos serviços (agora que uso await/async) e em futuras atualizações de serviços já existentes.

Exemplo SEM await/async - cada chamada bloqueia a thread - o método que usava até então:
constroiParede();// 1 segundo
constroiParede();// 1 segundo
constroiParede(); // 1 segundo
constroiParedeComPorta(); // 2 segundos
constroiTelhado(); 2 segundos
//total de tempo de execução: 7 segundos

Exemplo COM await/async - cada chamada que no seu método use Async, liberta a thread pelo que aqui acredito que existe um proveito em multiplos utilizadores ainda não verificado - nova versão após o refactoring:
await constroiParede();// 1 segundo
await constroiParede();// 1 segundo
await constroiParede(); // 1 segundo
await constroiParedeComPorta(); // 2 segundos
await constroiTelhado(); 2 segundos
//total de tempo de execução: 7 segundos

Exemplo COM await/async + paralelismo - para futuras implementações:
Task parede1 = constroiParede(); // 0 segundos
Task parede2 = constroiParede();// 0 segundos
Task parede3 = constroiParede(); // 0 segundos
Task parede4 = constroiParedeComPorta(); // 0 segundos
Task.WaitAll(parede1, parede2, parede3, parede4); // 2 segundos - o total de tempo de execução é do elo mais fraco e só podemos agregar métodos cujo resultado não dependa dos outros
await constroiTelhado(); 2 segundos
//total de tempo de execução: 4 segundos
 
Acessos a bases de dados sao também bons cenarios para utilizar o async/await. Basicamente, todas as operacoes I/O em que enquanto o recurso nao retorna, o CPU esta em idle. Isto é um conceito importante, visto que async/await nao cria novas threads e portanto nao executa nada em paralelo: simplesmente marca os metodos async para que, quando um await é finalizado, o programa volte a esse ponto. Por isto dizemos que metodos async nao bloqueiam a thread.

Tal como no exemplo dado aqui pela Microsoft, podes pensar nisto como a tarefa de fazer um pequeno almoco: enquanto os ovos estao a cozer, podes fazer as torradas, e enquanto as torradas estao a fazer podes fazer o cafe. Quando a torradeira saltar, voltas as torradas e por ai em diante. Repara que so tu estas a fazer o pequeno almoco (single thread), nao estas a olhar para a torradeira a espera que salte (thread bloqueada) nem contrataste alguem para te ajudar a fazer o pequeno almoco (varias threads).

No teu exemplo, cada vez que fazes um await, estas a dizer a thread para voltar a esse ponto assim que a tarefa assincrona retornar e que precisas desse valor para continuar. Neste caso, embora a thread nao bloqueie (e portanto com varios utilizadores vais ter beneficios em termos de performance), ela nao faz mais nada. Basicamente, pede dados a um recurso, espera que voltem, pede os proximos, etc...

O teu ultimo exemplo aproxima-se mais a como se utiliza o async/await: disparas todos os requests que necessitas e so os resolves (usas o await) quando realmente precisas deles. Sendo assim, poderias fazer algo do genero:

Código:
Task<Parede[]> paredesTask = constroiParedes(4)
... Outras coisas que precises de fazer aqui que nao dependem das paredes

Parede[] paredes = await paredesTask

... resto do metodo ...

O importante a reter aqui é que o tipo Task é uma promessa de que no futuro terá um valor ou um erro caso alguma coisa corra mal. Em javascript chamam-se Promises precisamente por isto.
Async/await nao cria threads novas, mas faz que threads de I/O em que o CPU ficaria idle, nao bloqueiem e torna possivel fazer varios requests de I/O simultaneamente sem ter de esperar que retornem (inclusivamente é possivel retornar uma task sem valor, se o metodo async retornar void. Este metodo é executado e simplesmente esquecido)

Espero que isto tenha ajudado!
Boa sorte!
 
Muito obrigado pelos teus comentários.

Async/await nao cria threads novas, mas faz que threads de I/O em que o CPU ficaria idle, nao bloqueiem e torna possivel fazer varios requests de I/O simultaneamente sem ter de esperar que retornem.
Sim, isto é o que eu tinha interpretado mesmo que para já não esteje a usar Task.WaitAll, ou seja, no cenário multiutilizador devo ter ganhos.

(inclusivamente é possivel retornar uma task sem valor, se o metodo async retornar void. Este metodo é executado e simplesmente esquecido)
Sim, eu reparei nisto.
Async void é suportado mas não recomendado.
Async void estou a usar em casos mais raros (executar e esquecer) pois mesmo que seja void, posso ter de esperar que o resultado seja concluido e nesse caso estou a usar Async Task.
 
Back
Topo