Em primeiro lugar, obrigado por suas amáveis palavras. É realmente um recurso incrível e estou feliz por ter sido uma pequena parte dele.
Se todo o meu código está lentamente se tornando assíncrono, por que não tornar tudo assíncrono por padrão?
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.
se o desempenho é o único problema, certamente algumas otimizações inteligentes podem remover a sobrecarga automaticamente quando não for necessária.
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 como x+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:
M();
N();
e M()
era void M() { Q(); R(); }
, e N()
foi void N() { S(); T(); }
, e R
e S
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 isso R()
vai acontecer antes ou depois S()
(a menos que M()
seja esperado, é claro ; mas é claro Task
que não precisa ser aguardado até depois N()
.)
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ão Lazy<T>
?" ou "por que não Nullable<T>
?" ou "por que não IEnumerable<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.