O padrão async-await do .net 4.5 está mudando o paradigma. É quase bom demais para ser verdade.
Tenho portado alguns códigos com muito IO para async-await porque o bloqueio é coisa do passado.
Muitas pessoas estão comparando async-await a uma infestação de zumbis e eu achei que é bastante preciso. O código assíncrono gosta de outro código assíncrono (você precisa de uma função assíncrona para aguardar em uma função assíncrona). Assim, mais e mais funções se tornam assíncronas e isso continua crescendo em sua base de código.
Alterar as funções para assíncrono é um trabalho um tanto repetitivo e sem imaginação. Jogue uma async
palavra - chave na declaração, envolva o valor de retorno Task<>
e pronto. É bastante perturbador o quão fácil é todo o processo, e em breve um script de substituição de texto irá automatizar a maior parte da "transferência" para mim.
E agora a pergunta ... Se todo o meu código está lentamente se tornando assíncrono, por que não tornar tudo assíncrono por padrão?
A razão óbvia que presumo é o desempenho. Async-await tem sua sobrecarga e código que não precisa ser assíncrono, de preferência não deveria. Mas se o desempenho é o único problema, certamente algumas otimizações inteligentes podem remover a sobrecarga automaticamente quando não for necessária. Eu li sobre a otimização "fast path" e me parece que ela sozinha deve cuidar da maior parte disso.
Talvez isso seja comparável à mudança de paradigma provocada pelos coletores de lixo. Nos primeiros dias do GC, liberar sua própria memória era definitivamente mais eficiente. Mas as massas ainda escolheram a coleta automática em favor de um código mais seguro e simples, que pode ser menos eficiente (e mesmo isso sem dúvida não é mais verdade). Talvez devesse ser o caso aqui? Por que nem todas as funções deveriam ser assíncronas?
fonte
async
(para cumprir algum contrato), esta provavelmente é uma má ideia. Você está obtendo as desvantagens do assíncrono (aumento do custo das chamadas de método; a necessidade de usarawait
no código de chamada), mas nenhuma das vantagens.Respostas:
Em primeiro lugar, obrigado por suas amáveis palavras. É realmente um recurso incrível e estou feliz por ter sido uma pequena parte dele.
Bem, você está exagerando; todo o seu código não está ficando assíncrono. Quando você adiciona dois inteiros "simples", não está esperando o resultado. Quando você soma dois inteiros futuros para obter um terceiro inteiro futuro - porque
Task<int>
é isso, é um inteiro ao qual você terá acesso no futuro - é claro que provavelmente estará aguardando o resultado.O principal motivo para não tornar tudo assíncrono é porque o objetivo de async / await é tornar mais fácil escrever código em um mundo com muitas operações de alta latência . A grande maioria de suas operações não é de alta latência, então não faz sentido sofrer o impacto de desempenho que atenua essa latência. Em vez disso, algumas de suas operações importantes são de alta latência e essas operações estão causando a infestação de zumbis de assíncronos em todo o código.
Em teoria, teoria e prática são semelhantes. Na prática, eles nunca são.
Deixe-me dar três pontos contra esse tipo de transformação seguida por uma passagem de otimização.
O primeiro ponto novamente é: assíncrono em C # / VB / F # é essencialmente uma forma limitada de passagem de continuação . Uma enorme quantidade de pesquisas na comunidade de linguagem funcional foi aplicada para descobrir maneiras de identificar como otimizar o código que faz uso intenso do estilo de passagem de continuação. A equipe do compilador provavelmente teria que resolver problemas muito semelhantes em um mundo em que "async" fosse o padrão e os métodos não assíncronos tivessem que ser identificados e dessincronizados. A equipe C # não está realmente interessada em resolver problemas de pesquisa abertos, então há grandes pontos contra.
Um segundo ponto contra é que o C # não tem o nível de "transparência referencial" que torna esses tipos de otimizações mais tratáveis. Por "transparência referencial" quero dizer a propriedade da qual o valor de uma expressão não depende quando ela é avaliada . Expressões como
2 + 2
são referencialmente transparentes; você pode fazer a avaliação em tempo de compilação, se quiser, ou adiá-la até o tempo de execução e obter a mesma resposta. Mas uma expressão comox+y
não pode ser movida no tempo porque x e y podem estar mudando com o tempo .O Async torna muito mais difícil raciocinar sobre quando um efeito colateral ocorrerá. Antes assíncrono, se você dissesse:
e
M()
eravoid M() { Q(); R(); }
, eN()
foivoid N() { S(); T(); }
, eR
eS
efeitos colaterais de produtos, então você sabe que efeito colateral de R acontece antes efeito colateral do S. Mas se você tiver,async void M() { await Q(); R(); }
então, de repente, isso sai pela janela. Você não tem garantia se issoR()
vai acontecer antes ou depoisS()
(a menos queM()
seja esperado, é claro ; mas é claroTask
que não precisa ser aguardado até depoisN()
.)Agora imagine que essa propriedade de não saber mais em que ordem os efeitos colaterais acontecem se aplica a cada trecho de código em seu programa, exceto aqueles que o otimizador consegue desassincronizar. Basicamente, você não tem mais ideia de quais expressões serão avaliadas em qual ordem, o que significa que todas as expressões precisam ser referencialmente transparentes, o que é difícil em uma linguagem como C #.
Um terceiro ponto contra é que você deve perguntar "por que o assíncrono é tão especial?" Se você vai argumentar que toda operação deveria ser realmente um,
Task<T>
então você precisa ser capaz de responder à pergunta "por que nãoLazy<T>
?" ou "por que nãoNullable<T>
?" ou "por que nãoIEnumerable<T>
?" Porque poderíamos facilmente fazer isso. Por que não deveria ser o caso de toda operação ser elevada a anulável ? Ou cada operação é calculada lentamente e o resultado é armazenado em cache para mais tarde , ou o resultado de cada operação é uma sequência de valores em vez de apenas um único valor . Em seguida, você deve tentar otimizar as situações em que sabe "ah, isso nunca deve ser nulo, para que eu possa gerar um código melhor" e assim por diante.A questão é: não está claro para mim se
Task<T>
é realmente especial para justificar tanto trabalho.Se esse tipo de coisa interessa a você, eu recomendo que você investigue linguagens funcionais como Haskell, que têm uma transparência referencial muito mais forte e permitem todos os tipos de avaliação fora de ordem e fazem cache automático. Haskell também tem um suporte muito mais forte em seu sistema de tipos para os tipos de "elevações monádicas" a que me referi.
fonte
async/await
porque algo oculto sob camadas de abstração pode ser assíncrono.O desempenho é um dos motivos, como você mencionou. Observe que a opção "fast path" à qual você vinculou melhora o desempenho no caso de uma Task concluída, mas ainda requer muito mais instruções e sobrecarga em comparação com uma única chamada de método. Dessa forma, mesmo com o "caminho rápido" em vigor, você está adicionando muita complexidade e sobrecarga a cada chamada de método assíncrona.
A compatibilidade com versões anteriores, bem como a compatibilidade com outras linguagens (incluindo cenários de interoperabilidade), também se tornaria problemática.
A outra é uma questão de complexidade e intenção. As operações assíncronas adicionam complexidade - em muitos casos, os recursos da linguagem escondem isso, mas há muitos casos em que criar métodos
async
definitivamente adiciona complexidade ao seu uso. Isso é especialmente verdadeiro se você não tiver um contexto de sincronização, pois os métodos assíncronos podem facilmente acabar causando problemas de threading inesperados.Além disso, existem muitas rotinas que não são, por natureza, assíncronas. Isso faz mais sentido como operações síncronas. Forçar
Math.Sqrt
para serTask<double> Math.SqrtAsync
seria ridículo, por exemplo, como não há nenhuma razão em tudo para que isso seja assíncrona. Em vez deasync
empurrar seu aplicativo, você acabaria seawait
propagando por toda parte .Isso também quebraria completamente o paradigma atual, além de causar problemas com propriedades (que são efetivamente apenas pares de métodos ... eles também iriam assíncronos?) E teria outras repercussões em todo o design da estrutura e da linguagem.
Se você estiver fazendo muito trabalho vinculado a IO, tenderá a descobrir que o uso
async
generalizado é uma ótima adição, muitas de suas rotinas o serãoasync
. No entanto, quando você começa a fazer o trabalho vinculado à CPU, em geral, fazer as coisasasync
não é bom - está escondendo o fato de que você está usando ciclos de CPU em uma API que parece ser assíncrona, mas não é necessariamente realmente assíncrona.fonte
await FooAsync()
mais simples do queFoo()
? E em vez de um pequeno efeito dominó algumas vezes, você tem um enorme efeito dominó o tempo todo e chama isso de uma melhoria?Desempenho à parte - assíncrono pode ter um custo de produtividade. No cliente (WinForms, WPF, Windows Phone) é uma vantagem para a produtividade. Mas no servidor, ou em outros cenários não UI, você paga produtividade. Você certamente não quer ficar assíncrono por padrão lá. Use-o quando precisar das vantagens de escalabilidade.
Use-o quando estiver no ponto ideal. Em outros casos, não.
fonte
Acredito que haja um bom motivo para tornar todos os métodos assíncronos se eles não forem necessários - extensibilidade. Os métodos de criação seletiva assíncronos só funcionam se seu código nunca evolui e você sabe que o método A () é sempre vinculado à CPU (você o mantém sincronizado) e o método B () é sempre vinculado à E / S (você o marca como assíncrono).
Mas e se as coisas mudarem? Sim, A () está fazendo cálculos, mas em algum momento no futuro você teve que adicionar registro lá, ou relatórios, ou retorno de chamada definido pelo usuário com implementação que não pode prever, ou o algoritmo foi estendido e agora inclui não apenas cálculos de CPU, mas também algum I / O? Você precisará converter o método para assíncrono, mas isso interromperia a API e todos os chamadores na pilha também precisariam ser atualizados (e eles podem até ser aplicativos diferentes de fornecedores diferentes). Ou você precisará adicionar a versão assíncrona junto com a versão de sincronização, mas isso não faz muita diferença - usar a versão de sincronização bloquearia e, portanto, dificilmente é aceitável.
Seria ótimo se fosse possível tornar o método de sincronização existente assíncrono sem alterar a API. Mas, na realidade, não temos essa opção, acredito, e usar a versão assíncrona, mesmo que não seja necessária no momento, é a única maneira de garantir que você nunca tenha problemas de compatibilidade no futuro.
fonte