Is it a hybrid type of thing? (e.g., does my .NET program use a stack until it hits an async call then switches over to some other structure until completed, at which point the stack is unwound back to a state where it can be sure of the next items, etc?)
Fondamentalmente sì.
Supponiamo di avere
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
Ecco una spiegazione estremamente semplificata di come le continuazioni vengono reificate. Il codice reale è considerevolmente più complesso, ma questa è l'idea.
Fai clic sul pulsante. Un messaggio è in coda. Il ciclo dei messaggi elabora il messaggio e chiama il gestore dei clic, inserendo l'indirizzo di ritorno della coda dei messaggi sullo stack. Cioè, la cosa che succede dopo il gestore è fatto è che il ciclo del messaggio deve continuare a funzionare. Quindi la continuazione del gestore è il ciclo.
Il gestore dei clic chiama Foo (), mettendo l'indirizzo di ritorno di se stesso nello stack. Cioè, la continuazione di Foo è il resto del gestore di clic.
Foo chiama Task.Delay, mettendo lo stesso indirizzo di ritorno sullo stack.
Task.Delay fa la magia necessaria per restituire immediatamente un'attività. La pila è spuntata e siamo di nuovo in Pippo.
Foo controlla l'attività restituita per vedere se è stata completata. Non è. La continuazione di attende è di chiamare Blah (), quindi Foo crea un delegato che chiama Blah () e firma che delegano come la continuazione dell'attività. (Ho appena fatto una piccola disapprovazione, l'hai capito? In caso contrario, lo riveleremo tra un momento.)
Foo crea quindi il proprio oggetto Task, lo contrassegna come incompleto e lo restituisce allo stack al gestore dei clic.
Il gestore dei clic esamina l'attività di Foo e scopre che è incompleto. La continuazione dell'attesa nel gestore consiste nel chiamare Bar (), quindi il gestore di clic crea un delegato che chiama Bar () e lo imposta come la continuazione dell'attività restituita da Foo (). Quindi restituisce lo stack al ciclo dei messaggi.
Il loop dei messaggi continua a elaborare i messaggi. Alla fine la magia del timer creata dal task di ritardo fa la sua cosa e invia un messaggio alla coda dicendo che la continuazione dell'attività di ritardo può ora essere eseguita. Quindi il ciclo dei messaggi chiama la continuazione dell'attività, mettendosi in pila come al solito. Quel delegato chiama Blah (). Blah () fa quello che fa e restituisce lo stack.
Ora cosa succede? Ecco il trucco. La continuazione dell'attività di ritardo non chiama solo Blah (). Deve anche attivare una chiamata a Bar () , ma quell'attività non conosce Bar!
Foo in realtà ha creato un delegato che (1) chiama Blah () e (2) chiama la continuazione dell'attività che Foo ha creato e restituita al gestore di eventi. È così che chiamiamo un delegato che chiama Bar ().
E ora abbiamo fatto tutto ciò che dovevamo fare, nell'ordine corretto. Ma non abbiamo mai interrotto l'elaborazione dei messaggi nel loop dei messaggi per molto tempo, quindi l'applicazione è rimasta reattiva.
That these scenarios are too advanced for a stack makes perfect sense, but what replaces the stack?
Un grafico di oggetti task che contengono riferimenti l'uno all'altro tramite le classi di chiusura dei delegati. Queste classi di chiusura sono macchine di stato che tengono traccia della posizione dell'attesa eseguita più recentemente e dei valori dei locali. Inoltre, nell'esempio fornito, una coda di azioni a livello globale implementata dal sistema operativo e il loop di messaggi che esegue tali azioni.
Esercizio: come pensi che tutto funzioni in un mondo senza loop di messaggi? Ad esempio, le applicazioni della console. attendere in un'app console è abbastanza diverso; puoi dedurre come funziona da quello che sai finora?
When I had learned about this years ago, the stack was there because
it was lightning fast and lightweight, a piece of memory allocated at
application away from the heap because it supported highly efficient
management for the task at hand (pun intended?). What's changed?
Le pile sono una struttura dati utile quando le durate delle attivazioni del metodo formano uno stack, ma nel mio esempio le attivazioni del gestore dei clic, Foo, Bar e Blah non formano una pila. E quindi la struttura dati che rappresenta quel flusso di lavoro non può essere uno stack; piuttosto è un grafico di attività e delegati allocati all'heap che rappresenta un flusso di lavoro. Le attese sono i punti nel flusso di lavoro in cui non è possibile progredire ulteriormente nel flusso di lavoro fino al completamento del lavoro iniziato in precedenza; mentre stiamo aspettando, possiamo eseguire un lavoro altro che non dipenda da quelle particolari attività avviate che sono state completate.
Lo stack è solo una serie di frame, dove i frame contengono (1) puntatori al centro delle funzioni (dove è avvenuta la chiamata) e (2) i valori delle variabili locali e delle temp. La continuazione dei compiti è la stessa cosa: il delegato è un puntatore alla funzione e ha uno stato che fa riferimento a un punto specifico nel mezzo della funzione (dove è avvenuta l'attesa) e la chiusura ha campi per ogni variabile locale o temporanea . I fotogrammi non formano più una bella matrice ordinata, ma tutte le informazioni sono uguali.