Vou responder às suas perguntas específicas abaixo, mas você provavelmente faria bem em simplesmente ler meus extensos artigos sobre como projetamos o rendimento e a espera.
https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/
https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/
https://blogs.msdn.microsoft.com/ericlippert/tag/async/
Alguns desses artigos estão desatualizados agora; o código gerado é diferente de várias maneiras. Mas isso certamente lhe dará uma ideia de como funciona.
Além disso, se você não entende como lambdas são gerados como classes de encerramento, entenda isso primeiro . Você não fará cara ou coroa de assíncrono se não tiver lambdas.
Quando a espera é alcançada, como o tempo de execução sabe qual parte do código deve ser executada a seguir?
await
é gerado como:
if (the task is not completed)
assign a delegate which executes the remainder of the method as the continuation of the task
return to the caller
else
execute the remainder of the method now
É basicamente isso. Await é apenas um retorno elegante.
Como sabe quando pode retomar de onde parou e como se lembra de onde?
Bem, como você faz isso sem esperar? Quando o método foo chama o método bar, de alguma forma nos lembramos de como voltar para o meio de foo, com todos os locais de ativação de foo intactos, não importa o que bar faça.
Você sabe como isso é feito no assembler. Um registro de ativação para foo é colocado na pilha; ele contém os valores dos habitantes locais. No momento da chamada, o endereço de retorno em foo é colocado na pilha. Quando a barra é concluída, o ponteiro da pilha e o ponteiro da instrução são redefinidos para onde precisam estar e foo continua de onde parou.
A continuação de um await é exatamente a mesma, exceto que o registro é colocado no heap pela razão óbvia de que a sequência de ativações não forma uma pilha .
O delegado que espera dá como continuação da tarefa contém (1) um número que é a entrada para uma tabela de pesquisa que fornece o ponteiro de instrução que você precisa para executar a seguir, e (2) todos os valores de locais e temporários.
Há algum equipamento adicional lá; por exemplo, no .NET é ilegal ramificar para o meio de um bloco try, então você não pode simplesmente inserir o endereço do código dentro de um bloco try na tabela. Mas esses são detalhes da contabilidade. Conceitualmente, o registro de ativação é simplesmente movido para o heap.
O que acontece com a pilha de chamadas atual, ela é salva de alguma forma?
As informações relevantes no registro de ativação atual nunca são colocadas na pilha em primeiro lugar; ele é alocado fora do heap desde o início. (Bem, os parâmetros formais são passados na pilha ou em registros normalmente e, em seguida, copiados em um local de heap quando o método começa.)
Os registros de ativação dos chamadores não são armazenados; a espera provavelmente vai voltar para eles, lembre-se, então eles serão tratados normalmente.
Observe que esta é uma diferença importante entre o estilo de passagem de continuação simplificado de await e as estruturas de continuação de chamada com corrente verdadeiras que você vê em linguagens como Scheme. Nessas línguas, toda a continuação, incluindo a continuação de volta para os chamadores, é capturada por call-cc .
E se o método de chamada fizer outras chamadas de método antes de esperar - por que a pilha não é sobrescrita?
Essas chamadas de método retornam e, portanto, seus registros de ativação não estão mais na pilha no ponto de espera.
E como diabos o runtime trabalharia por tudo isso no caso de uma exceção e uma pilha se desenrolaria?
No caso de uma exceção não capturada, a exceção é capturada, armazenada dentro da tarefa e lançada novamente quando o resultado da tarefa é buscado.
Lembra de toda aquela contabilidade que mencionei antes? Obter a semântica de exceção certa foi uma dor enorme, deixe-me dizer a você.
Quando o rendimento é alcançado, como o tempo de execução mantém o controle do ponto onde as coisas devem ser coletadas? Como o estado do iterador é preservado?
Da mesma maneira. O estado dos locais é movido para o heap e um número que representa a instrução na qual MoveNext
deve continuar na próxima vez que for chamada é armazenado junto com os locais.
E, novamente, há um monte de equipamentos em um bloco iterador para garantir que as exceções sejam tratadas corretamente.