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 + 2sã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+ynã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 Re Sefeitos 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 Taskque 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.