Ontem, eu estava dando uma palestra sobre o novo recurso "assíncrono" do C #, investigando em particular como era o código gerado e the GetAwaiter()
/ BeginAwait()
/ EndAwait()
chama.
Examinamos com detalhes a máquina de estado gerada pelo compilador C # e havia dois aspectos que não conseguimos entender:
- Por que a classe gerada contém um
Dispose()
método e uma$__disposing
variável, que nunca parecem ser usados (e a classe não é implementadaIDisposable
). - Por que a
state
variável interna é definida como 0 antes de qualquer chamadaEndAwait()
, quando 0 normalmente parece significar "este é o ponto de entrada inicial".
Suspeito que o primeiro ponto possa ser respondido fazendo algo mais interessante dentro do método assíncrono, embora se alguém tiver mais informações, eu ficaria feliz em ouvi-lo. Esta questão é mais sobre o segundo ponto, no entanto.
Aqui está uma parte muito simples do código de exemplo:
using System.Threading.Tasks;
class Test
{
static async Task<int> Sum(Task<int> t1, Task<int> t2)
{
return await t1 + await t2;
}
}
... e aqui está o código que é gerado para o MoveNext()
método que implementa a máquina de estado. Isso é copiado diretamente do Reflector - não corrigi os nomes indizíveis das variáveis:
public void MoveNext()
{
try
{
this.$__doFinallyBodies = true;
switch (this.<>1__state)
{
case 1:
break;
case 2:
goto Label_00DA;
case -1:
return;
default:
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
break;
}
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
this.<>1__state = 2;
this.$__doFinallyBodies = false;
if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
{
return;
}
this.$__doFinallyBodies = true;
Label_00DA:
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
this.<>1__state = -1;
this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
}
catch (Exception exception)
{
this.<>1__state = -1;
this.$builder.SetException(exception);
}
}
É longo, mas as linhas importantes para esta pergunta são as seguintes:
// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
Nos dois casos, o estado é alterado novamente mais tarde antes de ser obviamente observado ... então, por que defini-lo como 0? Se MoveNext()
fosse chamado novamente neste momento (diretamente ou via Dispose
), ele efetivamente iniciaria o método assíncrono novamente, o que seria totalmente inapropriado, tanto quanto eu sei ... se e MoveNext()
não for chamado, a mudança de estado é irrelevante.
Isso é simplesmente um efeito colateral do compilador que reutiliza o código de geração de blocos do iterador para assíncrono, onde pode ter uma explicação mais óbvia?
Isenção de responsabilidade importante
Obviamente, este é apenas um compilador CTP. Espero totalmente que as coisas mudem antes do lançamento final - e possivelmente até antes do próximo lançamento do CTP. Esta questão não está de forma alguma tentando afirmar que esta é uma falha no compilador C # ou algo assim. Eu só estou tentando descobrir se há uma razão sutil para isso que eu perdi :)
fonte
Respostas:
Ok, finalmente tenho uma resposta real. Eu meio que resolvi isso sozinho, mas só depois que Lucian Wischik, da parte VB da equipe, confirmou que realmente há uma boa razão para isso. Muito obrigado a ele - e visite o blog dele , que é demais.
O valor 0 aqui é apenas especial porque é não um estado válido, que você pode estar em um pouco antes da
await
em um caso normal. Em particular, não é um estado que a máquina de estado possa acabar testando em outro lugar. Acredito que o uso de qualquer valor não positivo funcionaria da mesma forma: -1 não é usado para isso, pois é logicamente incorreto, pois -1 normalmente significa "concluído". Eu poderia argumentar que estamos dando um significado extra ao estado 0 no momento, mas, no final das contas, isso realmente não importa. O objetivo desta pergunta foi descobrir por que o estado está sendo definido.O valor é relevante se a espera terminar em uma exceção capturada. Podemos voltar à mesma declaração de espera novamente, mas não devemos estar no estado que significa "Estou prestes a voltar dessa espera", pois caso contrário, todos os tipos de código seriam ignorados. É mais simples mostrar isso com um exemplo. Observe que agora estou usando o segundo CTP, portanto, o código gerado é um pouco diferente do da pergunta.
Aqui está o método assíncrono:
Conceitualmente,
SimpleAwaitable
pode haver qualquer expectativa - talvez uma tarefa, talvez outra coisa. Para os fins dos meus testes, ele sempre retorna false paraIsCompleted
e lança uma exceção emGetResult
.Aqui está o código gerado para
MoveNext
:Eu tive que mudar
Label_ContinuationPoint
para torná-lo um código válido - caso contrário, não está no escopo dagoto
declaração - mas isso não afeta a resposta.Pense no que acontece quando
GetResult
lança sua exceção. Vamos percorrer o bloco catch, incrementari
e, em seguida, dar a volta novamente (supondo quei
ainda seja menor que 3). Ainda estamos no estado em que estávamos antes daGetResult
ligação ... mas, quando entramos notry
bloco, precisamos imprimir "In Try" e ligarGetAwaiter
novamente ... e só faremos isso se o estado não for 1. Sem astate = 0
tarefa, ele usará o garçom existente e pulará aConsole.WriteLine
chamada.É um código bastante tortuoso de se trabalhar, mas isso só mostra o tipo de coisa em que a equipe precisa pensar. Estou feliz por não ser responsável por implementar isso :)
fonte
se fosse mantido em 1 (primeiro caso), você receberia uma ligação
EndAwait
sem uma ligação paraBeginAwait
. Se for mantido em 2 (segundo caso), você obterá o mesmo resultado apenas no outro garçom.Suponho que chamar o BeginAwait retorne false se já tiver sido iniciado (um palpite do meu lado) e mantenha o valor original para retornar no EndAwait. Se for esse o caso, funcionará corretamente, enquanto que se você o definir para -1, poderá ter um não inicializado
this.<1>t__$await1
para o primeiro caso.No entanto, isso pressupõe que BeginAwaiter não iniciará a ação em nenhuma chamada após a primeira e que retornará falso nesses casos. É claro que começar seria inaceitável, pois poderia ter efeito colateral ou simplesmente dar um resultado diferente. Também pressupõe que o EndAwaiter sempre retornará o mesmo valor, não importa quantas vezes seja chamado e que possa ser chamado quando BeginAwait retornar false (conforme a suposição acima)
Parece ser uma proteção contra as condições de corrida. Se alinharmos as declarações em que o movenext é chamado por um segmento diferente após o estado = 0 nas perguntas, ele parecerá com o abaixo
Se as suposições acima estiverem corretas, haverá algum trabalho desnecessário, como obter o sawiater e reatribuir o mesmo valor para <1> t __ $ waitit1. Se o estado fosse mantido em 1, a última parte seria:
além disso, se fosse definido como 2, a máquina de estados assumiria que já havia obtido o valor da primeira ação que seria falsa e uma variável (potencialmente) não atribuída seria usada para calcular o resultado
fonte
Poderia ser algo a ver com chamadas assíncronas empilhadas / aninhadas?
ou seja:
O delegado movenext é chamado várias vezes nessa situação?
Apenas um pontapé realmente?
fonte
MoveNext()
seria chamado uma vez em cada um deles.Explicação dos estados reais:
estados possíveis:
É possível que essa implementação queira apenas garantir que, se outra Chamada para o MoveNext, de onde quer que aconteça (enquanto espera), reavaliará toda a cadeia de estados novamente desde o início, para reavaliar resultados que, nesse meio tempo, já estão desatualizados?
fonte