Risponderò alle tue domande specifiche di seguito, ma probabilmente faresti bene a leggere semplicemente i miei ampi articoli su come abbiamo progettato il rendimento e l'attesa.
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/
Alcuni di questi articoli non sono aggiornati ora; il codice generato è diverso in molti modi. Ma questi ti daranno sicuramente l'idea di come funziona.
Inoltre, se non capisci come vengono generati i lambda come classi di chiusura, capiscilo prima . Non farai testa o croce asincrono se non hai lambda giù.
Quando viene raggiunto un wait, come fa il runtime a sapere quale parte di codice dovrebbe essere eseguita dopo?
await
viene generato come:
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
Fondamentalmente è così. Aspettare è solo un fantastico ritorno.
Come fa a sapere quando può riprendere da dove era stato interrotto e come ricorda dove?
Bene, come fai a farlo senza aspettare? Quando il metodo pippo chiama la barra del metodo, in qualche modo ricordiamo come tornare al centro di pippo, con tutte le parti locali dell'attivazione di pippo intatte, indipendentemente da cosa fa la barra.
Sai come si fa in assembler. Un record di attivazione per foo viene inserito nella pila; contiene i valori dei locali. Al punto della chiamata, l'indirizzo del mittente in foo viene inserito nello stack. Quando la barra è terminata, il puntatore dello stack e il puntatore dell'istruzione vengono reimpostati dove devono essere e foo continua dal punto in cui era stato interrotto.
La continuazione di un'attesa è esattamente la stessa, tranne per il fatto che il record viene messo nell'heap per l'ovvia ragione che la sequenza di attivazioni non forma uno stack .
Il delegato che attende fornisce come continuazione dell'attività contiene (1) un numero che è l'input di una tabella di ricerca che fornisce il puntatore all'istruzione che è necessario eseguire successivamente e (2) tutti i valori di locali e temporanei.
C'è qualche marcia in più lì dentro; per esempio, in .NET è illegale diramare nel mezzo di un blocco try, quindi non puoi semplicemente inserire l'indirizzo del codice all'interno di un blocco try nella tabella. Ma questi sono dettagli contabili. Concettualmente, il record di attivazione viene semplicemente spostato nell'heap.
Cosa succede allo stack di chiamate corrente, viene salvato in qualche modo?
Le informazioni rilevanti nel record di attivazione corrente non vengono mai messe in pila in primo luogo; viene allocato fuori dall'heap sin dall'inizio. (Bene, i parametri formali vengono passati normalmente sullo stack o nei registri e quindi copiati in una posizione di heap quando il metodo inizia.)
I record di attivazione dei chiamanti non vengono memorizzati; probabilmente l'attesa tornerà da loro, ricordate, quindi saranno trattati normalmente.
Si noti che questa è una differenza sostanziale tra lo stile di continuation crossing semplificato di await e le vere strutture di continuazione di chiamata con corrente che si vedono in linguaggi come Scheme. In quelle lingue l'intera continuazione, inclusa la continuazione nei chiamanti, viene catturata da call-cc .
E se il metodo chiamante effettua chiamate ad altri metodi prima che attenda, perché lo stack non viene sovrascritto?
Quelle chiamate al metodo vengono restituite e quindi i loro record di attivazione non sono più nello stack al momento dell'attesa.
E come diavolo avrebbe fatto il runtime a funzionare in tutto questo nel caso di un'eccezione e uno stack si svolgesse?
In caso di un'eccezione non rilevata, l'eccezione viene rilevata, archiviata all'interno dell'attività e rilanciata quando viene recuperato il risultato dell'attività.
Ricordi tutta quella contabilità che ho menzionato prima? Ottenere la giusta semantica delle eccezioni è stato un enorme problema, lascia che te lo dica.
Quando viene raggiunto il rendimento, in che modo il runtime tiene traccia del punto in cui le cose dovrebbero essere raccolte? Come viene preservato lo stato dell'iteratore?
Stessa strada. Lo stato delle locals viene spostato nell'heap e un numero che rappresenta l'istruzione alla quale MoveNext
dovrebbe riprendere la prossima volta che viene chiamato viene memorizzato insieme alle locals.
E ancora, c'è un sacco di attrezzi in un blocco iteratore per assicurarsi che le eccezioni siano gestite correttamente.