Por que precisamos da palavra-chave assíncrona?


19

Comecei a brincar com async / waitit no .Net 4.5. Uma coisa que me interessa inicialmente é: por que a palavra-chave assíncrona é necessária? A explicação que li foi que é um marcador para que o compilador saiba que um método aguarda alguma coisa. Mas parece que o compilador deve ser capaz de descobrir isso sem uma palavra-chave. Então, o que mais ele faz?


2
Aqui está um artigo de blog muito bom (série) que detalha a descrição da palavra-chave em C # e a funcionalidade relacionada em F #.
Paul

Eu acho que é principalmente um marcador para o desenvolvedor, e não para o compilador.
CodesInChaos


@ Rick obrigado, é isso que eu estava procurando. Faz todo o sentido agora.
ConditionRacer

Respostas:


23

Existem várias respostas aqui, e todas elas falam sobre o que os métodos assíncronos fazem, mas nenhuma delas responde à pergunta, e é por isso que asyncé necessária como uma palavra-chave incluída na declaração da função.

Não é "direcionar o compilador para transformar a função de uma maneira especial"; awaitsozinho poderia fazer isso. Por quê? Porque C # já tem um outro mecanismo onde a presença de uma palavra-chave especial no corpo do método faz com que o compilador para executar extremo (e muito semelhante a async/await) transformações sobre o corpo do método: yield.

Exceto que essa yieldnão é sua própria palavra-chave em C #, e entendendo por que explicará asynctambém. Diferente da maioria dos idiomas que suportam esse mecanismo, no C # você não pode dizer que yield value;você precisa dizer yield return value;. Por quê? Como foi adicionado ao idioma depois que o C # já existia e era bastante razoável supor que alguém, em algum lugar, poderia ter usado yieldcomo o nome de uma variável. Mas como não havia um cenário pré-existente <variable name> returnsintaticamente correto, yield returnfoi adicionado ao idioma para possibilitar a introdução de geradores, mantendo 100% de compatibilidade com o código existente.

E é por isso que asyncfoi adicionado como um modificador de função: para evitar a quebra de código existente usado awaitcomo nome de variável. Como nenhum asyncmétodo já existia, nenhum código antigo é invalidado e, no novo código, o compilador pode usar a presença da asyncmarca para saber que awaitdeve ser tratada como uma palavra-chave e não como um identificador.


3
Isso é basicamente o que este artigo também diz (originalmente publicado por @svick nos comentários), mas ninguém nunca o postou como resposta. Levou apenas dois anos e meio! Obrigado :)
ConditionRacer

Isso não significa que, em alguma versão futura, o requisito de especificar a asyncpalavra - chave possa ser eliminado? Obviamente, seria uma interrupção do BC, mas pode-se pensar que isso afeta muito poucos projetos e é um requisito direto para atualização (ou seja, alterar o nome de uma variável).
Quolonel Questions

6

altera o método de um método normal para um objeto com retorno de chamada que requer uma abordagem totalmente diferente para geração de código

e quando algo drástico como esse acontece, é costume significar claramente (aprendemos essa lição com C ++)


Então isso é realmente algo para o leitor / programador, e não tanto para o compilador?
ConditionRacer

@ Justin984 definitivamente ajuda ao sinal às necessidades compilador algo especial acontecer
catraca aberração

Acho que estou curioso para saber por que o compilador precisa. Não poderia simplesmente analisar o método e ver que aguardar está em algum lugar no corpo do método?
ConditionRacer

ele ajuda quando você quer agendar várias asyncs ao mesmo tempo para que eles não executar um após o outro, mas ao mesmo tempo (se tópicos estão disponíveis pelo menos)
catraca aberração

@ Justin984: Nem todo método que retorna Task<T>realmente tem as características de um asyncmétodo - por exemplo, você pode apenas executar alguma lógica comercial / de fábrica e delegar a outro método retornado Task<T>. asyncOs métodos têm um tipo de retorno Task<T>na assinatura, mas na verdade não retornam a Task<T>, eles apenas retornam a T. Para descobrir tudo isso sem a asyncpalavra - chave, o compilador C # precisaria fazer todos os tipos de inspeção profunda do método, o que provavelmente reduziria bastante a velocidade e levaria a todos os tipos de ambiguidades.
Aaronaught

4

A idéia geral com palavras-chave como "assíncrono" ou "não seguro" é remover a ambiguidade de como o código que eles modificam deve ser tratado. No caso da palavra-chave assíncrona, ele diz ao compilador para tratar o método modificado como algo que não precisa retornar imediatamente. Isso permite que o encadeamento em que esse método é usado continue sem ter que esperar pelos resultados desse método. É efetivamente uma otimização de código.


Fico tentado a reduzir o voto porque, enquanto o primeiro parágrafo está correto, a comparação com as interrupções está incorreta (na minha opinião).
Paul

Eu os vejo como algo análogo em termos de propósito, mas vou removê-lo por uma questão de clareza.
World Engineer

As interrupções são mais parecidas com eventos do que com código assíncrono - elas causam uma alternância de contexto e são concluídas antes que o código normal seja retomado (e o código normal também pode ser completamente ignorante da execução), enquanto que com o código assíncrono o método pode não foi concluído antes que o encadeamento de chamada seja retomado. Entendo o que você estava tentando dizer através de diferentes mecanismos que exigem implementações diferentes, mas as interrupções simplesmente não me ajudaram a ilustrar esse ponto. (Um para o restante, entre.)
Paul

0

OK, aqui está minha opinião.

Existe algo chamado corotinas que é conhecido há décadas. ("Knuth e Hopper" -classe "por décadas") São generalizações de sub-rotinas , não apenas como elas obtêm e liberam o controle na instrução de início e retorno da função, mas também em pontos específicos ( pontos de suspensão ). Uma sub-rotina é uma corotina sem pontos de suspensão.

Eles são muito fáceis de implementar com macros C, conforme mostrado no artigo a seguir sobre "protothreads". ( http://dunkels.com/adam/dunkels06protothreads.pdf ) Leia. Eu vou esperar...

A conclusão é que as macros criam um grande switche um caserótulo em cada ponto de suspensão. Em cada ponto de suspensão, a função armazena o valor do caserótulo imediatamente a seguir , para que ele saiba onde retomar a execução na próxima vez em que for chamado. E retorna o controle para o chamador.

Isso é feito sem modificar o fluxo aparente de controle do código descrito no "protothread".

Imagine agora que você tem um grande loop chamando todos esses "protothreads" por sua vez e pode executar simultaneamente "protothreads" em um único thread.

Essa abordagem tem duas desvantagens:

  1. Você não pode manter o estado nas variáveis ​​locais entre as retomadas.
  2. Você não pode suspender o "protothread" de uma profundidade de chamada arbitrária. (todos os pontos de suspensão devem estar no nível 0)

Existem soluções alternativas para ambos:

  1. Todas as variáveis ​​locais devem ser puxadas para o contexto do protothread (contexto que já é necessário porque o protothread deve armazenar seu próximo ponto de retomada)
  2. Se você sentir que realmente precisa chamar outro prototread de um protothread, "crie" um protothread infantil e suspenda até a conclusão da criança.

E se você tivesse suporte do compilador para executar o trabalho de reescrita que as macros e a solução alternativa, bem, você poderia escrever seu código de protothread exatamente como pretendia e inserir pontos de suspensão com uma palavra-chave.

E é isso que asynce awaitsão todos sobre: a criação (Stackless) coroutines.

As corotinas em C # são reificadas como objetos da classe (genérica ou não genérica) Task.

Acho essas palavras-chave muito enganadoras. Minha leitura mental é:

  • async como "suspensível"
  • await como "suspender até a conclusão de"
  • Task como "futuro ..."

Agora. Realmente precisamos marcar a função async? Além de dizer que deve acionar os mecanismos de reescrita do código para tornar a função uma rotina, ele resolve algumas ambiguidades. Considere este código.

public Task<object> AmIACoroutine() {
    var tcs = new TaskCompletionSource<object>();
    return tcs.Task;
}

Supondo que isso asyncnão seja obrigatório, isso é uma corrotina ou uma função normal? O compilador deve reescrevê-lo como uma rotina ou não? Ambos podem ser possíveis com diferentes semânticas eventuais.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.